Loops and Variable Indirection in Puppet

A while back I wrote about writing some custom Puppet facts, one of which was to enumerate the enslaved interfaces for each bonded network interface on a Linux host. So for example your facter output might contain the following facts:

interfaces => bond0,eth0,eth1,eth2,sit0
ipaddress_bond0 => 192.168.0.1
ipaddress_eth2 => 10.0.0.1
slaves_bond0 => eth0,eth1

The bottom fact is my addition. Why do I even want that? Well, I have Nagios monitoring the host via the bonded interface so I’ll know if that breaks completely as the host will be down but I’d also like to know if any one of the enslaved interfaces are down as even though the host should still be up thanks to the bonded interface doing its magic, a cable might have been unplugged or something more serious might have happened. On a modern Linux host with the /sys filesystem, you can do the following to see if an interface is connected to hardware:

# cat /sys/class/net/eth0/carrier
1

That value will flip to 0 should the cable be pulled, etc. so I wrote a really basic Nagios test that exits with the OK or CRITICAL value based on that. Now to write the Puppet manifest, in english the logic sounds pretty simple:

“Loop through each interface reported by the $interfaces fact, if the interface looks like a bonded interface, (i.e. it matches /bond\d+/), then loop through each interface reported by the $slaves_$interface fact which should now exist and export a Nagios service definition for it.”

First problem, there’s no such thing as a loop in Puppet. You can get around this by using a definition:

define match_bonded_interfaces {
    if $name =~ /bond\d+/ {
        ...
    }
}
 
$array_of_interfaces = split($interfaces, ',')
 
match_bonded_interfaces { $array_of_interfaces: }

This will call the definition for each value in the array with $name being set to the current element each time.

The second problem that I hit after this was I needed to access my custom fact for the interface to enumerate the enslaved interfaces. Not wanting to hardcode the interface name and without thinking I had tried to do the following:

define match_bonded_interfaces {
    if $name =~ /bond\d+/ {
        $slaves = ${slaves_$name}
    }
}

It seemed semi-logical at the time until I actually came to try it out and found it didn’t at all do what I had hoped. After a few further failed attempts at guessing some undocumented syntax to do what I wanted, I gave up thinking there was no way to do variable indirection in Puppet, in other words, accessing a variable by the value of another variable.

To me this seemed kind of odd. After all, quite a lot of the facts reported by facter are structured like this:

list_of_things => key1,key2,key3
value_key1 => foo
value_key2 => bar
value_key3 => baz

And if there’s any trace of programmer in you, you’re going to want to access those in a way that scales anywhere between 1 and n keys, rather than hardcode each possible key value and the associated variable names. Something deep inside would stop me from committing something like the following:

if $interfaces =~ /\beth0\b/ {
    notice("IP address of eth0 is ${ipaddress_eth0}")
}
 
if $interfaces =~ /\beth1\b/ {
    notice("IP address of eth1 is ${ipaddress_eth1}")
}
 
etc.

But annoyingly, that was the only way I could utilise my new fact, and besides most of the servers only had one bonded interface, but it was still annoying nonetheless so I was pleasantly surprised when a friend pointed me towards this blog entry which offered a possible solution, you use Puppets inline_template function to evaluate a small ERB template fragment like so:

define monitor_interface {
    @@nagios_service {
        ...
    }
}
 
define match_bonded_interfaces {
    if $name =~ /bond\d+/ {
        $slave_fact = "slaves_${name}"
        $slaves = inline_template('<%= scope.lookupvar(slave_fact) %>')
        $array_of_slaves = split($slaves, ',')
        monitor_interface { $array_of_slaves: }
    }
}
 
$array_of_interfaces = split($interfaces, ',')
 
match_bonded_interfaces { $array_of_interfaces: }

I feel dirty now.

Tags: , ,

5 Responses to “Loops and Variable Indirection in Puppet”

  1. Stephen P. Schaefer says:

    Thanks for this. Since the puppet maintainers have pulled Red Hat support for the “interface type”, I’m using your example to come up with my own partial support.

    • Vincent janelle says:

      You might want to check out adrien-network, it’s pretty awesome at abstracting out the network configs.

  2. Nathan says:

    Perhaps I’m missing something obvious here but couldn’t you do $slaves = “slaves_${name}”?

    That seems to work for me,

    class test(){
    $fooname = “eth0”

    $slaves = “slaves_${fooname}”

    notice(“slaves => $slaves”)
    }

    notice: Scope(Class[Test]): Slaves are slaves_eth0

  3. Alister says:

    Thank you for this very clear and concise snippet Much easier than the online docs or #puppet 🙂

Leave a Reply