Sysadmin stories

Ramblings about my job and trade

A Puppet Module for Managing BIRD

Do you know the BIRD Internet Routing Daemon? If not, and if you have any kind of need for dynamic routing, you should probably have a look at it. From my experience, its OSPF implementation is at least much more stable than the one in Quagga. Now, I needed a Puppet module to manage it, didn’t I?

Existing choices

“Existing” is probably at least a bit misleading here, as the only other module I am aware of hadn’t been published yet when I began writing my own (actually I just discovered its existence 10 minutes ago).

From a cursory glance at Sebastien’s code, I quite like his idea of invoking birdc and birdc6 directly for restarting his services. However his module does not manage the configuration files at all; mine does and the feature gets real-life use.

So where is the code?

Here. The module code itself probably does not comply fully with the GPL at the moment (it is missing the legalese in headers); I expect to fix it as soon as I can find some time. I also expect to advertise it on Puppet Forge as soon as I add the missing bits (Modulefile and so on).

At the moment this module is running in production on a couple CentOS 5 hosts with custom-built BIRD packages; it should also work on recent enough versions of Debian (“recent enough” meaning Jessie). The situation is a bit more complicated with Wheezy; basically, package bird 1.4.0-1~bpo70+1 from Wheezy backports includes both bird and bird6 daemons, whereas package bird 1.3.7 from Wheezy itself only installs the IPv4 version but recommends the IPv6 package. The Puppet module installs package bird only; it should work if the package drags the bird6 daemon along with it.

OK, this was the code, now what?

What about the docs?

Seems like I’m writing them right now. :–)

The most basic use is probably the obvious one:

include bird

This will give you working instances of bird and bird6 which do, well, basically nothing. If you want them to do something, you can add config snippets in /etc/bird.conf.d/ and /etc/bird6.conf.d/ directories and restart the daemons (kill -HUP will do).

What if for some reason you don’t want both protocols? Simple enough, you can disable the useless one:

class { 'bird':
  enable_ipv4 => false,
}

By default, the “main” IP address of the host (that is, the one returned by the fact ipaddress) is used as the router ID. You can of course change this:

class { 'bird':
  router_id => '192.0.2.42',
}

There are also a couple defines for generating some of those configuration snippets I was talking about a bit earlier. For instance, you can add an OSPF instance:

bird::protocol::ospf { 'VPNv4':
  ip_protocol     => 'ipv4',
  ospf_interfaces => {'eth0' => {password => $eth0_ospf_password},
                     {'eth1' => {password => $eth1_ospf_password},
                     {'eth2' => {}}},
  stub_interfaces => 'tap0',
}

Or set up RAdv on an IPv6 interface:

$radv_prefixes = { "$vpn_interface" => $ipv6_subnet }
class { 'bird::protocol::radv':
  prefixes => $radv_prefixes,
}

Basically, these defines are enough for my real-life use cases. If anything more specific is needed, I can just craft a snippet by hand and add it to the configuration directories.

What next?

The basic functionality is here. The module itself, on the other hand, is missing quite a lot of things, including documentation and tests.

Maybe adding simple BGP support would be a good idea too; I don’t need it right now but who knows?

And, of course, publishing the module on the PuppetLabs Forge should come as soon as possible. I’ll let you know. ;–)

Puppet: Embedding Data in Modules

How would you manage admin accounts on ten or so Puppet-enabled Linux hosts? Even if I had LDAP it would not be an option; basically the admins have to be able to log in even on a broken OS, and LDAP tends to be easier to break than /etc/passwd.

So, let’s use Puppet

It would be quite easy to create a module with just a bunch of user resources, but I wanted to avoid Puppet boilerplate as much as possible.

Fortunately, the stdlib module from PuppetLabs provides us with a couple of useful functions to load data from a YAML file located somewhere inside our module, namely get_module_path and loadyaml. So we can list our users in a YAML file and use these two functions in addition to create_resources to generate our user resources.

We should now be able to create a minimal working “users” module. The directory tree looks like this:

+- data/
|    admins.yaml
+- manifests/
     init.pp

The file init.pp is very short; it just needs to call the three functions I mentioned earlier with the right parameters. Here it is in full:

class users {
  $module_path = get_module_path($module_name)
  $admins = loadyaml("${module_path}/data/admins.yaml")
  create_resources(user, $admins)
}

Now we can add our user accounts in admins.yaml:

alice:
  gecos: Alice
  groups:
  - adm
  - sudo
  shell: /bin/bash
  password: $6$XXXXXXXXXXXXXXXXXXXXXXXXX
bob:
  gecos: Robert Tables
  groups:
  - adm
  shell: /bin/csh
  password: $6$YYYYYYYYYYYYYYYYYYYYYYYYYYYY

So we’re done? More or less. We now have our admin accounts, but we probably need to deploy their SSH public keys. On my real-life machines, sshd does not allow password authentication so accounts without SSH keys would be useless.

Adding SSH keys

The obvious way out would be to manage one SSH key for each user with Puppet and let the users themselves push any additional public keys they might want to use. This would be quite easy, mostly a duplicate of what we just did for user accounts.

In this case it would also be suboptimal. So let’s do better.

Basically we need a resource type which can manage a user account with any number of public keys. Each public key is defined by an encryption type (ssh-dss or ssh-rsa), a label (usually user@host but this is absolutely not mandatory) and the key itself. The obvious way to store our keys is in a hash, indexed by the key label (which is used by the Puppet type ssh_authorized_keys as its namevar):

ssh_keys['user@host'] = {
  type => 'ssh-dss' or 'ssh-rsa'
  key  => 'AAA.........'
}

We can now define a new type, which we will call users::sshuser and which will be a wrapper around the native user type with added support for the user’s SSH public keys.

While we’re at it, we will allow users to change their own passwords; basically we will set the user’s password only if it is not already set and ignore our password parameter otherwise. Finally we will set a couple of default properties (gid and shell) to match my local policy, and force the managehome parameter of the native user type to true.

Here is the code for our new defined type:

define users::sshuser (
  $ensure   = present,
  $uid      = undef,
  $gid      = 'users',
  $groups,
  $gecos    = '',
  $shell    = '/bin/bash',
  $password = '',
  $ssh_keys,
) {

  $username = $title

  user { $username:
    ensure     => $ensure,
    managehome => true,
    gid        => $gid,
    comment    => $gecos,
    shell      => $shell,
  }

  if $groups {
    User[$username] { groups => $groups, }
  }

  if $uid {
    User[$username] { uid => $uid, }
  }

  if $password and ($ensure == present) {
    exec { "/usr/sbin/usermod -p '$password' $username":
      path    => '/bin:/usr/bin',
      require => User[$username],
      unless  => "grep $username /etc/shadow | cut -f 2 -d : | grep -v '!'"
    }
  }

  if ($ensure == present) {
    validate_hash($ssh_keys)
    create_resources(ssh_authorized_key, $ssh_keys, {ensure => present, user => $username})
  }

}

Of course we have to modify our main users class:

class users {
  $module_path = get_module_path($module_name)
  $admins = loadyaml("${module_path}/data/admins.yaml")
  create_resources(users::sshuser, $admins)
}

And of course add our SSH keys to our YAML file:

alice:
  gecos: Alice
  groups:
  - adm
  - sudo
  shell: /bin/bash
  password: $6$XXXXXXXXXXXXXXXXXXXXXXXXX
  ssh_keys:
    alice@example.com:
      type: ssh-rsa
      key: AAAA...............
bob:
  gecos: Robert Tables
  groups:
  - adm
  shell: /bin/csh
  password: $6$YYYYYYYYYYYYYYYYYYYYYYYYYYYY
  ssh_keys:
    bob@example.net:
      type: ssh-rsa
      key: AAAA...............
    btables@myhost:
      type: ssh-dss
      key: AAAA..........

Now, we’re done. Adding a user or an SSH key just requires editing the YAML file and the relevant changes will be applied on every host on the next Puppet run, and we didn’t hardcode anything in the Puppet code itself (well, as a matter of fact, we did hardcode a few policy points, but we could easily have made them into configuration parameters).

We can even remove a user’s account or a public key by setting ensure: absent in the right place in the YAML file. To be honest, while this was a “real” feature for user accounts (I planned to disable accounts from the start), for SSH public keys this is mostly an unintended side-effect of the way create_resource works.

Further ideas

I happen to like the solution I described in this article because the data is kept in the module itself. I can think of a couple of alternatives for using another canonical source instead.

The point is moot in this case but I can think of another instance where I had an LDAP directory to get my user data from, even though I still wanted local users for various reasons (including the fact that some hosts are not allowed to talk to the directory servers). In this case I wrote another small module around create_resources and I used my ENC to get user data from my LDAP directory and feed it to this module.

We could also use a custom Hiera backend rather than a static file or an ENC. I must confess I didn’t look in this direction, mostly because Hiera wasn’t yet a given when I began thinking of this pattern. I guess Hiera would be a cleaner solution than a custom ENC, at least conceptually, provided you’re fluent enough in Ruby; I’m not. ;–) And the data would no longer be embedded in the module, which is a design decision I intend to keep for the module I’m talking about here.