building or appending to an array using a lambda in Puppet

Tripped over a challenge shifting KVM guest definitions into hiera.

I’m using the cirrax/libvirt module which offers up this way to define a guest.  (I’ve modified the example in the module documentation.)

libvirt::domain { 'my-domain':
  max_memory     => '2000',
  cpus           => 2,
  boot           => 'hd',
  disks          => [{'type'   => 'file',
                      'device' => 'disk',
                      'source' => {'dev' => '/var/lib/libvirt/images/disk1.img'},
                      'bus'    => 'virtio',
                      'driver' => {'name'  => 'qemu',
                                   'type'  => 'raw',
                                   'cache' => 'none',
                                   },
                     },
                     {'type'   => 'file',
                      'device' => 'disk',
                      'source' => {'dev' => '/var/lib/libvirt/images/disk2.img'},
                      'bus'    => 'virtio',
                      'driver' => {'name'  => 'qemu',
                                   'type'  => 'raw',
                                   'cache' => 'none',
                                   },
                     },
                     ],
  interfaces     => [{'network' => 'lan'},],
  autostart      => true,
}

I wanted to:

  • Abstract this out into hiera.
  • Simplify the hiera disk definitions.
  • Support creating the disk within puppet, which means also defining the size.
  • Support more than one disk.

So, I need to loop over the guests list, and then within each one, potentially loop over the disks.

The hiera would look like this:

profile::kvm::guests::list:
  test1:
    max_memory: '1024'
    cpus: 1
    disks:
      'test1-1':
        size: 5120
        type: 'file.raw'
  test2:
    max_memory: '1024'
    cpus: 1
    disks:
      'test2-1':
        size: 4096
        type: 'file.raw'
      'test2-2':
        size: 100
        type: 'file.raw'

The disk loop needs to:

  • Validate the data passed from hiera.
  • Potentially support different disk types in the future.
  • For raw disk types, use exec to create the file if required.
  • Fabricate the disk definition that the libvirt module requires.

The challenge is that data structures in Puppet can only be created once because manifests are not sequential. You can’t keep appending things to arrays any more than you can keep redefining a scalar. Which came first? Neither. They happen at the same time, and so can only happen once.

Puppet map function

The fix is the map function. Hat tips: stack overflow, puppet docs.

Technically it doesn’t append to the array. It builds it, but in my mind I was thinking in terms of appending, and trying to work out how to use array appending or concatenation. So, I named this blog post to help other folk who are thinking the same way!

The code below is simplified and probably contain bugs. style issues, etc. I’ve edited it heavily, removing things like:

  • Dependencies between resources.
  • Code which handled assigning defaults when hiera didn’t supply values (RAM, CPU etc.)
  • Some of the abstraction I’ve done.

In so far so it demonstrates the nested loops and the map function, it’s the same as code I’ve got working.

Yup, ‘$diskdefinition’ is just dropped there on its own. It works! I think it’s a Ruby thing; the examples I’ve found of Ruby scripts to generate facts do a similar sort of thing to return the fact.

class profile::kvm::guests (
  String $imagefs         = '/var/lib/libvirt',
  String $networkname     = 'lan',
  Optional[Hash] $list    = undef,
) {
  $list.each |String $guestname, Hash $guestdef| {
    $maxmem = $guestdef['max_memory']
    $cpus = $guestdef['cpus']
    if has_key($guestdef,'disks'){
      # here's the map function
      # the loop needs to spit something out, and this gets appended to $alldisks
      $alldisks = $guestdef['disks'].map |String $filen, Hash $diskdef| {

        # bit of validation
        if !has_key($diskdef,'type'){
          fail("disk ${filen} missing type key")
        }
        if !has_key($diskdef,'size'){
          fail("disk ${filen} missing size key")
        }

        # support different disk flavours, with validation of the supplied value
        case $diskdef['type'] {
          'file.raw': {

            # create the disk if required
            $diskfqn="${imagefs}/${filen}.img"
            exec {"create ${diskfqn}":
              command   => "/bin/dd if=/dev/zero of='${diskfqn}' bs=1M count=${diskdef[size]}",
              creates   => "${diskfqn}",
              logoutput => true,
              timeout   => 0,
            }

            # create the hash that defines the disk
            $diskdefinition = {'type'   => 'file',
                               'device' => 'disk',
                               'source' => {'file' => $diskfqn },
                               'bus'    => 'virtio',
                               'driver' => {'name'  => 'qemu',
                                            'type'  => 'raw',
                                            'cache' => 'none',
                                            },
                               }
          }
          default:    {
            fail("unsupported disk type ${diskdef['type']}")
          }
        } # case

        # this is how we get the disk definition onto the array, via map
        $diskdefinition
      } # disk loop
    }
    else {
      fail("disks hash missing for ${guestname}")
    }

    # create guest

    libvirt::domain { $guestname:
      max_memory     => $maxmem,
      cpus           => $cpus,
      boot           => 'hd',
      disks          => $alldisks,
      interfaces     => [{'network' => 'lan',}],
      autostart      => true,
    }
  } # loop over each guest 
}

Troubleshooting

Troubleshooting misbehaving input data or code can be a pain.

However, I tripped over a neat bit of code which can be dropped in to help.

I would remark out the call to libvirt::domain and add the following in the same place. Note that it writes out the current scope, so if you’ve got a bunch of different scopes (such as loops) you need to put it in the right place and ensure that the filenames are made unique, otherwise you’ll get a resource conflict.

    file { "/tmp/guest_${guestname}.yaml":
      content   => inline_template('<%= scope.to_hash.to_yaml %>'),
      show_diff => false,
    }

This is based on puppetcookbook.

At the bottom of that file you’ll find the variables defined in the manifest and within that loop, such as

guestname: test1
maxmem: '1024'
cpus: 1
alldisks:
- type: file
  device: disk
  source:
    file: "/var/lib/libvirt/images/test1-1.img"
  bus: virtio
  driver:
    name: qemu
    type: raw
    cache: none

You’ll find list in there as well; what hiera has provided.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s