As a first step on the road to evaluating different configuration management systems, I set up two VMs to test out puppet.

Puppet is conveniently packaged within Debian so client and server installation was very simple. By default, puppet clients will look for a host named puppet on the local network, so by naming the server sensibly the two started talking to each other. puppetca --sign [client hostname] on the server started the ball rolling.

My first test configuration was to add a user to the sudoers file. With CF Engine 2, I would have achieved this by using editfiles to check for a regular expression and insert a line if necessary. I stumbled across some CF Engine-style definitions for puppet in a Debian Administration article (taken originally from puppet's own wiki, which have since been moved/deleted) and achieved it using

append_if_no_such_line { sudo:
  file => "/etc/sudoers",
  line => "jon ALL=(ALL) ALL"

I'm a little concerned that this approach to problem solving is no better than CF Engine 2, and in fact worse than how I'd do it with CF Engine 2: if the host had a line with a different quantity of whitespace, such as jon ALL=(ALL) ALL, it won't match exactly and a duplicate line will be appended, whereas with CF Engine 2 I would have checked for something like ^jon \+ALL=(ALL) \+ALL$.

I set up my work laptop as another test node, and used puppet to install some packages that I commonly use:

class jon-desktop {
  package { ['icedove', 'git-core', 'vim-gnome', 'vinagre', 'build-essential',
             'devscripts', 'subversion', 'git-buildpackage', 'mutt',
             'offlineimap', 'ascii', 'gitk', 'chromium-browser'
            ]: ensure => installed }

This worked fine, but is a little verbose and the package list is awkward to maintain buried inside the puppet syntax. I spent some time trying to see whether I could populate a puppet list based on the contents of a file: then I could list the packages one-per-line in a separate file. I haven't found a way of doing that yet.

Using my laptop as a second node also exposed some interesting puppet client behaviour. Once I went off-site, the laptop tried to look up puppet on foreign networks (such as my home network). I would have assumed that, once the strong SSL association had been made, the client would try to connect to the FQDN of the server. This probably isn't a serious problem in practice, as we probably won't need to rely on nodes off-site talking to the configuration server, but I hope that the client doesn't trust any host that happens to answer to puppet...

I fixed this by hacking my hosts file to include an entry for puppet. I could maintain this hack using Puppet itself, which would protect me against stale definitions if any renumbering takes place. I could also have added a line

server =

in /etc/puppet/puppet.conf, which is a more robust way of solving the problem.



About I haven't found a way of doing that yet. sure it will be more elegant in Perl, Python, Ruby, whatever, but can be done using just bash:

inigo@crono:~/tmp $ cat packages.txt 
# a comment

# empty lines


# end
inigo@crono:~/tmp $ ( IFS=$'\n' arr=( $(while read -r pkg; do echo \ \'$pkg\';
done<<< "$(grep -vE '(#|^$)' packages.txt)" ) ); ( IFS=, ;
printf "class jon-desktop {\n  package { [%s]: ensure => installed }\n}\n" \
"${arr[*]}" ; ) ; )
class jon-desktop {
  package { [ 'icedove', 'git-core', 'vim-gnome', 'vinagre', 'build-essential',
'devscripts', 'subversion']: ensure => installed }
inigo@crono:~/tmp $

Greetings, poisonbit (Iñigo).


Ick, I'd not realised that you could do cfengine in puppet -- how revolting.

Anyway, moving swiftly on, you should look at Augeas, and puppet-augeas, which lets you do stuff like this:

    augeas { "sudoer-jon":
        context =>  "/files/etc/sudoers/spec[user = 'jon']",
        changes => [
            "set user jon",
            "set host_group/host ALL",
            "set host_group/command ALL",
            "set host_group/command/runas_user ALL"

I've only written a few of these rules, so there are probably bugs in that, but the point is that Augeas reads config files into a tree structure, and lets you search that tree, and set new bits in it -- if your changes section actually changes anything, it then gets translated back into the format of the relevant config file.

An example I have tested, that adds an extra file to one's logrotate setup if not already present, is as follows:

    augeas { "dhcp-logrotate":
        context =>  "/files/etc/logrotate.d/rsyslog/rule[schedule = 'daily']",
        changes => [
            "ins file after file[last()]",
            "set file[last()] /var/log/dhcpd.log",
        onlyif => "match file[. = '/var/log/dhcpd.log'] size == 0",
This adds the dhcpd.log file entry after the last file entry in the daily set, but only if it's not already findable.

Phil Hands