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.