puppet

puppet is made by Reductive Labs and released under the GNU Public License. It is a system to manage configuration files and services. It is written in ruby.

There are several products out there that do the same job as puppet, the main competitor is Cfengine. There is also the open source version of Red Hat satellite server spacewalk. All these systems have their various benefits and pitfalls. I find puppet to be simpler to configure and very easy to extend. One possible pitfall is that it is written in Ruby, and I have almost no experience in ruby. That said, it's a very simple language to read to quite easy to follow (unlike PERL).

To configure puppet, you install the puppet-server rpm or build from source. The rpms are available for centos, RHEL (via EPEL) and Fedora. In the previous section we downloaded the puppet rpms from EPEL and installed them in our local repository.

The steps to configure puppet are

puppetmaster

The puppetmaster is installed from the puppet-server rpm. Using yum, you should get the following dependencies installed: augeas-libs; puppet; ruby-augeas; and ruby-shadow (or possibly more). After installing the puppet-server rpm, you can chkconfig puppetmaster on and cd to /etc/puppet
[root@server0 ~]# chkconfig puppetmaster on
[root@server0 ~]# cd /etc/puppet
[root@server0 puppet]# ls
fileserver.conf  manifests  puppet.conf
[root@server0 puppet]# ls manifests
[root@server0 puppet]# 
Both the server and client versions use the /etc/puppet directory. Both are configured from puppet.conf. The default configuration of puppet.conf will be sufficient at this point, but the manifests directory is completely empty. This is possibly the biggest complain with puppet, you start with a blank slate. It is up to you to populate the manifest directory. The only file you will need is site.pp in that directory. The site.pp file names all the other files you wish to include in your manifest.

The puppetmaster listens on port 8140, so we'll need to add that port to our firewall

[root@server0 ~]# iptables -I RH-Firewall-1-INPUT -p tcp -m state --state NEW,ESTABLISHED,RELATED --dport 8140 -j ACCEPT
[root@server0 ~]# iptables-save >/etc/sysconfig/iptables
We cannot start the puppetmaster yet, we have an empty manifests directory, we'll start building the contents of that directory now.

manifests, classes and types

The manifests are files that contain a series of functions, types, classes and control logic to apply a set of actions to nodes. In the manifests you define classes which are a grouping of type instances and functions. Classes are then applied to nodes.

There are many types predefined, for a complete list of all types and their usage, see the documentation. Common types used are:

To create a new class, use the class keyword. As a simple example, we'll create a class which creates a file in root's home directory called hello, with the word "world" in it.

class test {
	file {"/root/hello":
		content => "world",
		mode => 644,
		owner => root,
		group => root
	}		
}
To have this class applied to a node, we need to assign the class to the node with the node and include keywords.
node client15 {
	include test
}
Put these definitions in site.pp and start puppetmaster.
[root@server0 manifests]# cat site.pp
class test {
	file {"/root/hello":
		content => "world",
		mode => 644,
		owner => root,
		group => root
	}		
}

node client15 {
	include test
}
[root@server0 manifests]# service puppetmaster start
Starting puppetmaster:                                     [  OK  ]
Now that we have a minimal site.pp installed, we can configure our client and test the configuration. We'll configure clients from kickstart later, but to test the install at this point, login to client15 and execute puppet manually.
[root@client15 ~]# puppetd --no-daemonize  --server server0.example.com --test --no-splay
info: Creating a new certificate request for client15.example.com
info: Creating a new SSL key at /var/lib/puppet/ssl/private_keys/client15.example.com.pem
warning: peer certificate won't be verified in this SSL session
notice: Did not receive certificate
notice: Set to run 'one time'; exiting with no certificate
Our client successfully connected to the puppetmaster but it failed to retrieve a catalog (what puppet calls the collection of actions to perform on the client). This is because our ssl key was not signed by the puppetmaster. Back on the puppetmaster, we'll sign the key for client15 and then try our test again.

[root@server0 ssl]# puppetca --list
client15.example.com
[root@server0 ssl]# puppetca --sign client15.example.com
Signed client15.example.com
Back on client 15
[root@client15 ~]# puppetd --no-daemonize  --server server0.example.com --test --no-splay
warning: peer certificate won't be verified in this SSL session
notice: Got signed certificate
info: Caching catalog at /var/lib/puppet/localconfig.yaml
notice: Starting catalog run
notice: //Node[client15]/test/File[/root/hello]/content: defined 'content' as '{md5}7d793037a0760186574b0282f2f435e7'
notice: //Node[client15]/test/File[/root/hello]/owner: defined 'owner' as 'root'
notice: //Node[client15]/test/File[/root/hello]/group: defined 'group' as 'root'
notice: //Node[client15]/test/File[/root/hello]/mode: defined 'mode' as '644'
info: Creating state file /var/lib/puppet/state/state.yaml
notice: Finished catalog run in 0.03 seconds
[root@client15 ~]# cat /root/hello
world[root@client15 ~]# 
Now that we have verified that puppet is working, we'll make a proper site.pp file and some useful classes.

site.pp

In our configuration, we will keep node definitions in the file nodes.pp, any puppet functions we define in functions.pp and then any classes we implement in files named for the class they contain. Our initial site will be the following:
# site.pp
import "functions.pp"

# define nodes
import "nodes.pp"

# define classes
import "base.pp"

functions.pp

Function definitions are created with the define keyword. Functions can go in any of the pp files in the manifests directory. I choose to keep them separate for accounting purposes. My current functions.pp only has two functions in it, and they are very simple. The first one is a redefinition of the file type with defaults for my installation.
define remotefile($owner = root, $group= root, $mode, $server = "server0.example.com", $cls="base", $backup = false, $recurse = false) {
	file {
	    $name:
            mode => $mode,
            owner => $owner,
            group => $group,
            backup => $backup,
            source => "puppet://$server/$cls/$name"
	}
}
The name of the function is remotefile, since we imported functions.pp in our site.pp, we can use remotefile in any other file we include from site.pp. The file type in puppet can have a remote location that is given in the source option. Our function defines a default location of puppet://server0.example.com/base/$name where name is the name of the file. We also set the owner and group to root by default. Without using this function, copying an /etc/resolv.conf file from our puppetmaster to a client would look like this:
file {"/etc/resolv.conf":
    mode => 644,
    owner => root,
    group => root,
    backup => false
    source => "puppet://server0.example.com/base/etc/resolv.conf"
}
Using the function we can slim this down to:
remotefile { "/etc/resolv.conf": mode=> 644 }
Next we will define our nodes and a default class for puppetmaster to apply to the nodes.

nodes.pp

Nodes are defined with the node keyword. In a node you call the relevant classes with include class. The special node named default is used to specify which class to apply to all nodes that aren't addressed specifically. Our simple setup will have only the default definition.

node default {
    include base
}

With this defined, we'll create another manifest for the class base.

base.pp

In this class we'll define those changes that we want on all the machines in our organization. We'll start the class with a class definition and include anything we with to define. As a practical example, we'll create an ssh key for the user signer and install that key on any puppet clients that register with our puppetmaster. We'll also write out the puppet configuration file in /etc/sysconfig (should we need to update it at a later date, it will already be under puppet control).

[root@server0 manifests]# su - signer
[signer@server0 ~]$ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/signer/.ssh/id_rsa): 
Created directory '/home/signer/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/signer/.ssh/id_rsa.
Your public key has been saved in /home/signer/.ssh/id_rsa.pub.
The key fingerprint is:
35:b7:84:6d:34:8c:76:9a:8d:7c:3e:4a:e8:c1:1e:fd signer@server0.example.com
[signer@server0 ~]$ cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAr9rnu0jbSPipuZI2umz/v73jeRTjxlX9D7cHSFIaJUUShFSelFUfojkjl4ri4m4qc40icArMa4NMGZ9d3y+ZqMqeIPZVtJKEqkn2E9GJS36N13H75DwVPv4KE2oLR9Zk4T8HovLr50tWbJr5/G6VfwwybR3q6HdJSO7liAKmrJwFokev1fsmiZQX+rADL8XB+gZ/9FsFIi4F4YKsLGQz78CSf/jZ71qNC5Y4HniVQDv6RmZp+koHT6hOPKTuUD/VOWXHxoLc9c6ypkeSMaINvNHDvmUsbp+rNppiZPKnsDFoh3fL4h5pFKJ1DAYjOdnhLKJgwLzmBq7qfYpd/PEw2Q== signer@server0.example.com
[signer@server0 ~]$ exit
[root@server0 manifests]# cat base.pp 
class base {
	remotefile { "/etc/sysconfig/puppet": mode => 644 }
        service { puppet: ensure => true, enable => true, hasrestart => true }
	ssh_authorized_key { "signer":
		ensure => present,
		type => "ssh-rsa",
		key => "AAAAB3NzaC1yc2EAAAABIwAAAQEAr9rnu0jbSPipuZI2umz/v73jeRTjxlX9D7cHSFIaJUUShFSelFUfojkjl4ri4m4qc40icArMa4NMGZ9d3y+ZqMqeIPZVtJKEqkn2E9GJS36N13H75DwVPv4KE2oLR9Zk4T8HovLr50tWbJr5/G6VfwwybR3q6HdJSO7liAKmrJwFokev1fsmiZQX+rADL8XB+gZ/9FsFIi4F4YKsLGQz78CSf/jZ71qNC5Y4HniVQDv6RmZp+koHT6hOPKTuUD/VOWXHxoLc9c6ypkeSMaINvNHDvmUsbp+rNppiZPKnsDFoh3fL4h5pFKJ1DAYjOdnhLKJgwLzmBq7qfYpd/PEw2Q==",
		name => "signer@example.com",
		target => "/root/.ssh/authorized_keys"
	}
}
[root@server0 manifests]# 
Now we have to create /etc/sysconfig/puppet that we referenced in our call to remotefile.
[root@server0 puppet]# pushd /var/lib/puppet/files
/var/lib/puppet/files /etc/puppet
[root@server0 puppet]# mkdir -p files/base/etc/sysconfig
[root@server0 puppet]# mkdir facts
[root@server0 puppet]# cd files/base/etc/sysconfig
[root@server0 sysconfig]# cat <puppet
> PUPPET_SERVER=server0.example.com
> PUPPET_EXTRA_OPTS=--factsync
> EOF
[root@server0 files]# popd
/etc/puppet
Next, we'll configure the puppet fileserver to serve out files that are stored in /var/lib/puppet/files/base

fileserver.conf

We configure the puppet fileserver from fileserver.conf
[root@server0 puppet]# cat fileserver.conf
[base]
	path /var/lib/puppet/files/base
	allow *.example.com
	allow 192.168.0.0/24

[facts]
	path /var/lib/puppet/facts
	allow *.example.com
	allow 192.168.0.0/24
Now we have defined the base and facts shares. The facts share is used by facter, something we'll talk about later, but we might as well configure it's file share while we are here. At this point we can restart the puppetmaster, but when new clients connect, they have to wait for us to manually sign their keys. We'll configure puppet to automatically sign keys using autosign.conf

autosign.conf

autosign.conf is a very simple file that simply lists the objects which will be automatically signed. In our case we can add example.com and the 192.168.0.0/24 subnet for automatic signing

[root@server0 puppet]# cat autosign.conf 
*.example.com
192.168.0.0/24

At this point new clients can connect and (provided they are in example.com or our subnet) they will have their keys automatically signed. They will all be placed in the base class by default. Our next step is to go back to our kickstart and have puppet configured with kickstart.

puppet in kickstart

We'll configure puppet in the %post section of our kickstart. We'll call puppet directly (not as a service) and have it create /etc/sysconfig/puppet for us (since we configured that file in our base class). This is one way to do this, another is to put the puppet configuration into the kickstart file and ensure that puppet is chkconfig'd on from there. The latter has the advantage that if the puppetmaster is unavailable at install time, the machine will still have the correct configuration.

Method 1:

%post
chvt 3
echo "executing post install"

echo hostname for puppet is $HOSTNAME
puppetd --fqdn=$HOSTNAME --test --no-splay --server=server0.example.com --onetime --verbose --factsync
echo "type enter to continue"
read enter_key

chvt 1

After installing your machine should change to virtual terminal 3 (chvt 3) and echo "executing post install" followed by the output from puppet
info: Retrieving facts
info: Caching catalog at /var/lib/puppet/localconfig.yaml
notice: Starting catalog run
notice: //Node[default]/base/Ssh_authorized_key[signer]/ensure: created
1,11c1,2
< # The puppetmaster server
< #PUPPET_SERVER=puppet
< 
< # If you wish to specify the port to connect to do so here
< #PUPPET_PORT=8140
< 
< # Where to log to. Specify syslog to send log messages to the system log.
< #PUPPET_LOG=/var/log/puppet/puppet.log
< 
< # You may specify other parameters to the puppet client here
< #PUPPET_EXTRA_OPTS=--waitforcert=500
---
> PUPPET_SERVER=server0.example.com
> PUPPET_EXTRA_OPTS=--factsync
notice: //Node[default]/base/Remotefile[/etc/sysconfig/puppet]/File[/etc/sysconfig/puppet]/source: replacing from source puppet://server0.example.com/base//etc/sysconfig/puppet with contents {md5}87dd2effcdc742da03df3ab010c03436
notice: //Node[default]/base/Service[puppet]/enable: enable changed 'false' to 'true'
notice: Finished catalog run in 0.23 seconds
type enter to continue

Method 2:

%post
chvt 3
echo "executing post install"

cat >/etc/sysconfig/puppet <<EOF
PUPPET_SERVER=server0.example.com
PUPPET_EXTRA_OPTS=--factsync
EOF
chkconfig puppet on

echo hostname for puppet is $HOSTNAME
puppetd --fqdn=$HOSTNAME --test --no-splay --server=server0.example.com --onetime --verbose --factsync
echo "type enter to continue"
read enter_key

chvt 1
Using this method, if the puppetmaster is down when this client goes to execute puppetd, it will fail but puppet will still be configured and will work when the machine is rebooted. I prefer to use Method 2 just in case.

At this point your clients are installing from scratch and getting configured by puppet automatically from kickstart. The bulk of the work is done, what you now need to do is go through your machine configurations and translate all the changes into puppet. This may take a while, but in the end it is well worth it. There may be some changes that are difficult to translate into puppet, for those hard to make changes that require some advanced sed/awk work, augeas may be the answer. We'll look at that in the next section.

For making changes on the fly (not waiting for puppet clients to check-in) you will need to configure func (or use cluster ssh or something similar). Now, on to augeas.