2/2: An introduction to using Puppet

Posted by Steve on Fri 25 May 2007 at 11:39

In our previous introduction to Puppet we covered installing the client and server, such that a small LAN could be configured centrally. Here we'll demonstrate some of the things that can be done with such a setup.

In our brief introduction to installing Puppet we demonstrated a very simple manifest file, called site.pp. This is the master configuration file which is pushed by puppet to each managed host, and will be used to specify what actions to carry out. (What is a manifest file? Well it is a list of instructions on what to carry out in a declarative fashion which is why it isn't called a script or a program file.)

There are many actions which may carried out by puppet:

Last time we demonstrated the following manifest file (Saved in /etc/puppet/manifests/site.pp):

# fixup permissions on sudo
class sudo {
    file { "/etc/sudoers":
        owner => root,
        group => root,
        mode  => 440,
    }
}

node default {
     include sudo
}

This example shows several things:

  1. Basic class definition with "class sudo ..".
  2. The use of the "file" object.
  3. The definition of actions to carry out on all hosts.

The definition of the class we'll gloss over, as it is treated pretty comprehensively in Puppet documentation, however the file section is worthy of note.

The file type is one of several types that Puppet understands - in much the same way that CFEngine understands the use of copy: sections. It allows you to do things such as:

(The Puppet type reference shows more types with examples for several of them.)

Copying Files

Copying files from the master server can be conducted with the file type which we've just introduced. To perform a copy is very similar to fixing up permissions which we saw in the example above. All we need to do is add a source for the file, which will be somewhere upon our master server.

Previously we enabled the Puppet server, running upon the host vain.my.flat, to serve files to clients from the directory /etc/puppet/files so we'll continue working with that prefix.

Assuming we wanted each host upon our LAN to receive a copy of /etc/sudoers from our central server we'd write the following in /etc/puppet/manifests/site.pp:


#
#  Class to pull /etc/sudoers from our master server.
#
class sudo {
    file { "/etc/sudoers":
        owner  => root,
        group  => root,
        mode   => 440,
        source => "puppet://vain.my.flat/files/etc/sudoers"
    }
}

node default {
     include sudo
}

Now we just need to place the file in the correct location upon the master server:

root@vain:~# mkdir -p /etc/puppet/files/etc/
root@vain:~# cp /etc/sudoers  /etc/puppet/files/etc/

This will now ensure that the file sudoers is available beneath our file-sharing root, and can be installed upon all machines.

If you wanted to have an action carried out after copying a file, but only if it was changed, you could use something like this:

class aptsetup {

      file { "/etc/apt/sources.list",
             owner  => root, group => root, mode => 644,
             source => "puppet://puppet/files/etc/apt/sources.list.etch 
      }

      exec { subscribe-echo:
               command     => "/usr/bin/apt-get -q -q update",
               logoutput   => false,
               refreshonly => true,
               subscribe   => file["/etc/apt/sources.list"]
      }
}

Now you can just "include aptsetup" in the definition of your node(s) and they will receive a copy of the sources.list file from the remote server. If that is updated upon the server then they will receive a refreshed copy and they will automatically run "apt-get update".

Including Classes

So far we've seen a very simple layout with all the content located in the single site.pp file. That can quickly become unmanageable, so it is recommended that you break your configuration down into a set of self-contained classes.

A basic site.pp could look like this:

import "classes/*.pp"

node default {
     include sudoers
     include aptsetup
}

Now we can create the files "/etc/puppet/manifests/classes/sudo.pp" & "/etc/puppet/manifests/classes/aptsetup.pp" with the contents we've seen before and all will continue to work properly.

Defining Nodes

One thing that should be apparent in our example so far is that we've not really split our code up to handle any complex situations where managed nodes differ. Instead we've just demonstrated the use of "node default" which means that our actions have been carried out upon all machines using this server.

Here is an example of a more complex setup for two machines (etch-builder.my.flat and sid-builder.my.flat).

Each machine will get the appropriate sources.list file setup, in addition to some common activity applied to both machines, and any other machines which have been configured by not named separetely:

class sudo {
    file { "/etc/sudoers":
        owner  => root,
        group  => root,
        mode   => 440,
        source => "puppet://vain.my.flat/files/etc/sudoers"
    }
}

class common {
   # Remove upstream sources.list
   # We manage this in sources.list.d per distro
   tidy { "/etc/apt/sources.list":
          age => '0s',
        }
}

class etch {
   file{ "/etc/apt/sources.list.d/etch.list":
         mode   => 644,
         source => "puppet://vain.my.flat/files/etc/apt/sources.etch"
       }
}

class sid {
   file{ "/etc/apt/sources.list.d/sid.list":
         mode   => 644,
         source => "puppet://vain.my.flat/files/etc/apt/sources.sid"
       }
}

node default {
     # all host get these actions
     include sudo
}

node etch-builder inherits default {
     include common
     include etch
}
node sid-builder inherits default {
     include common
     include sid
}

Making Puppet more CFEngine-like

Several actions familiar to the users of CFengine are missing, such as DeleteLinesMatching and AppendIfNoSuchLine which allow simple text file manipulation.

Thanks to this Puppet wiki page we can find a simple solution which covers most cases.

Create the file /etc/puppet/manifests/classes/cfengine.pp with the following contents:


define append_if_no_such_line($file, $line, $refreshonly = 'false') {
   exec { "/bin/echo '$line' >> '$file'":
      unless      => "/bin/grep -Fxqe '$line' '$file'",
      path        => "/bin",
      refreshonly => $refreshonly,
   }
}

define delete_lines($file, $pattern) {
   exec { "sed -i -r -e '/$pattern/d' $file":
      path   => "/bin",
      onlyif => "/bin/grep -E '$pattern' '$file'",
   }
}

Now in your site.pp file you can have the following:

import "classes/*.pp"

node default {
   delete_lines{ removecfengine:
           file    => "/etc/motd",
           pattern => "^.*CFEngine.*$" }

   append_if_no_such_line{ motd:
           file => "/etc/motd",
           line => "Configured with Puppet!"}
}

I'll close the second part of this introduction here, even though I've barely scratched the surface of what Puppet can do for you. If you're interested in learning more I'd suggest you look at the documentation at the Puppet Wiki.

The IRC channel was friendly and helpful when I joined; allowing me to get my first change committed to the project in only minutes!


This article can be found online at the Debian Administration website at the following bookmarkable URL:

This article is copyright 2007 Steve - please ask for permission to republish or translate.