Concatenating Data

Concatenating Data

Written: 2017-07-21
Author: WhatsARanjit
Link: WhatsARanjit/data_fragments

The problem

Let’s say we have several network devices that use JSON for their configuration. It looks something like this:

{
  "switch1": "192.168.1.2"
}

Also we have a single upstream switch that needs to dynamically discover downstream devices. The configuration on the upstream switch looks like this:

{
  "switch1": "192.168.1.2",
  "switch2": "192.168.1.3"
}

An easy way in Puppet for discovery and adding to a file might be using exported resources and the puppetlabs/concat module. When a new device comes online and gets its first catalog, it will send a fragment to the Puppet master. When the upstream switch checks in next, it will collect all those fragments and piece them together into a file.

But if we export these fragments:

# From switch1
{
  "switch1": "192.168.1.2"
}

# From switch2
{
  "switch2": "192.168.1.3"
}

..we get…

{
  "switch1": "192.168.1.2"
}
{
  "switch2": "192.168.1.3"
}

This isn’t valid JSON and your switch will surely break. You can try starting with an inital fragment of { and an ending fragment of }. But because JSON hates trailing commas, you’ll need a clever way of comma-separating each key without leaving a trailing comma. This means you can’t just do this:

concat_fragment { 'header':
  content => '{',
  order   => 1,
}
concat_fragment { 'footer':
  content => '}',
  order   => 100,
}
# From switch1
@@concat_fragment { 'nope1':
  content => '"switch1": "192.168.1.2",',
  order   => 2,
}
# From switch2
@@concat_fragment { 'nope2':
  content => '"switch2": "192.168.1.3",',
  order   => 2,
}

The fix

puppetlabs/concat is great for concatenating plain-text, but isn’t able to work with data structures. Looking into the concat module’s code, it’s very close to being able to do so. I have an alternate module at WhatsARanjit/data)fragments while a pull request sits in wait. It adds a format attribute that allows you to specify that you’re working with json or yaml. I added json-pretty in there as well. So let’s start with 2 dummy data files:

first.json

{
  "one": {
    "oneA": "A",
    "oneB": {
      "oneB1": "1",
      "oneB2": "2"
    }
  },
  "two": [
    "twoA",
    "twoB"
  ]
}

second.json

{
  "one": {
    "oneA": "B",
    "oneB": {
      "oneB1": "2",
      "oneB3": "3"
    }
  },
  "two": [
    "twoA",
    "twoC"
  ]
}

We can write puppet code like this:


$output = '/tmp/json.json'
$dp     = '/path/to/data'

data_file { $output:
  ensure => present,
  tag    => 'stuff',
  format => 'json',
  force  => true,
}
data_fragment { 'first':
  content => file("${dp}/examples/data/first.json"),
  order   => '01',
  target  => $output,
  tag     => 'stuff',
}
data_fragment { 'second':
  content => file("${dp}/examples/data/second.json"),
  order   => '10',
  target  => $output,
  tag     => 'stuff',
}

I’ve also added a force attribute. In the case that you have overlapping keys, you’ll get a parser fail letting you know there is conflicting data. Using the force attribute tells Puppet to merge the data, keeping the data that has the highest ranking order. So now order is not just how to piece the data together, but which dataset takes priority over another. Running the above code produces an output like this:

{
  "one": {
    "oneA": "A",
    "oneB": {
      "oneB1": "1",
      "oneB2": "2",
      "oneB3": "3"
    }
  },
  "two": [
    "twoA",
    "twoB",
    "twoC"
  ]
}

For format, you can also choose plain, which is the default and it will function just like concat would. The module lives here.