Keep your Puppet manifests under version control

It’s a good idea to keep your Puppet manifests and other configuration files under version control, for example Subversion or CVS. To do this, just set up the Puppet Master the way you want, and then import the whole of /etc/puppet into Subversion:

$ svn import /etc/puppet https://www.your-svn-server-here.com/svn/puppet/trunk

You can then check out a working copy somewhere else:

$ svn co https://www.your-svn-server-here.com/svn/puppet/trunk puppet

Edit it, then commit your changes, and update the master copy on the server:

$ svn up /etc/puppet

The Puppet master automatically detects that its configuration files have changed.

Remember when you create new certificates, you are modifying the Puppet master’s working copy, so you need to commit these changes every so often. This has the added benefit that if you should lose the Puppet master server, you can easily recreate it by just checking out a copy of the Puppet tree into /etc/puppet.

Configuration Versioning

From version 0.25.0, a new configuration option, config_version, is now available:

config_version = /usr/local/bin/return_version

The option allows you to specify a command that returns a version for the configuration that is being applied to your hosts. The command should return a string, such as a version number or name.

Puppet then runs this command at compile time. Each resource is marked with the value returned from this command. This value is also added to the log instance, serialised and sent along with any report generated. This allows you to parse your report output and ascertain which configuration version was used to generate the resource.

Using Hooks

Hooks let you extend the value of Subversion (or Git, CVS, etc) to perform error checking, stage files and even produce audit trails.

SVN Hooks

SVN Pre-Commit Hook

To catch syntax errors and other basic problems, you can use a Subversion pre-commit hook like this:

#!/bin/sh
# SVN pre-commit hook to check Puppet syntax for .pp files
# Modified from http://mail.madstop.com/pipermail/puppet-users/2007-March/002034.html
REPOS="$1"
TXN="$2"
tmpfile=`mktemp`
export HOME=/
SVNLOOK=/usr/bin/svnlook
$SVNLOOK changed -t "$TXN" "$REPOS" | awk '{print $2}' | grep '\.pp$' | while read line
do
    $SVNLOOK cat -t "$TXN" "$REPOS" "$line" > $tmpfile
    if [ $? -ne 0 ]
    then
        echo "Warning: Failed to checkout $line" >&2
    fi
    puppet --color=false --confdir=/tmp --vardir=/tmp --parseonly --ignoreimport $tmpfile >&2
    if [ $? -ne 0 ]
    then
        echo "Puppet syntax error in $line." >&2
        exit 2
    fi
done
res=$?
rm -f $tmpfile
if [ $res -ne 0 ]
then
    exit $res
fi

If you get errors like this when committing:

/usr/lib/ruby/site_ruby/1.8/puppet/defaults.rb:102:in `handle': private method `split' called for nil:NilClass (NoMethodError)

It’s because the PATH environmental variable doesn’t exist. It doesn’t matter what it is, but puppet wants it to be there. Try adding PATH=“” somewhere in the top of the pre-commit script.

SVN Post-Commit Hook

Using a post-commit hook can be handy if you want your commits to automatically be seen by Puppet, e.g. you don’t have to do the last step shown above (svn up). Also, by integrating cvsspam you can provide an audit trail with nicely formatted, coloured diffs.

Here’s a simplified example of a post-commit that simply updates the files (previous checked-out) in /etc/puppet.

#!/bin/sh
REPOS="$1"
REV="$2"
svn up /etc/puppet

Of course much more can be done here, but that is a nice start.

Git Hooks

Git Update Hook

To catch syntax errors and other basic problems, you can use a server-side Git update hook like this:

#!/bin/bash

NOBOLD="\033[0m"
BOLD="\033[1m"
BLACK="\033[30m"
GREY="\033[0m"
RED="\033[31m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"
MAGENTA="\033[35m"
CYAN="\033[36m"
WHITE="\033[37m"

# V +1007

# Peff helped:
# http://thread.gmane.org/gmane.comp.version-control.git/118626

syntax_check="puppet --color=false --confdir=/tmp --vardir=/tmp --parseonly --ignoreimport"
tmp=$(mktemp /tmp/git.update.XXXXXX)
log=$(mktemp /tmp/git.update.log.XXXXXX)
tree=$(mktemp /tmp/git.diff-tree.XXXXXX)

git diff-tree -r "$2" "$3" > $tree

echo
echo diff-tree:
cat $tree

exit_status=0

while read old_mode new_mode old_sha1 new_sha1 status name
do
  # skip lines showing parent commit
  test -z "$new_sha1" && continue
  # skip deletions
  [ "$new_sha1" = "0000000000000000000000000000000000000000" ] && continue
  # Only test .pp files
  if [[ $name =~ [.]pp$ ]]
  then
    git cat-file blob $new_sha1 > $tmp
    set -o pipefail
    $syntax_check $tmp 2>&1 | sed 's|/tmp/git.update.......:\([0-9]*\)$|JOJOMOJO:\1|'> $log
    if [[ $? != 0 ]]
    then
      echo
      echo -e "$(cat $log | sed 's|JOJOMOJO|'\\${RED}${name}\\${NOBOLD}'|')" >&2
      echo -e "For more details run this: ${CYAN} git diff $old_sha1 $new_sha1 ${NOBOLD}" >&2 
      echo
      exit_status=1
    fi
  fi
done < $tree

rm -f $log $tmp $tree
exit $exit_status

Git Pre-Commit Hook

To catch syntax errors and other basic problems, you can use a client-side Git pre-commit hook like this:

#!/bin/sh

syntax_errors=0
error_msg=$(mktemp /tmp/error_msg.XXXXXX)

if git rev-parse --quiet --verify HEAD > /dev/null
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi

# Get list of new/modified manifest and template files to check (in git index)
for indexfile in `git diff-index --diff-filter=AM --name-only --cached $against | egrep '\.(pp|erb)'`
do
    # Don't check empty files
    if [ `git cat-file -s :0:$indexfile` -gt 0 ]
    then
        case $indexfile in
            *.pp )
                # Check puppet manifest syntax
                git cat-file blob :0:$indexfile | puppet --color=false --parseonly --ignoreimport > $error_msg ;;
            *.erb )
                # Check ERB template syntax
                git cat-file blob :0:$indexfile | erb -x -T - | ruby -c 2> $error_msg > /dev/null ;;
        esac
        if [ "$?" -ne 0 ]
        then
            echo -n "$indexfile: "
            cat $error_msg
            syntax_errors=`expr $syntax_errors + 1`
        fi
    fi
done

rm -f $error_msg

if [ "$syntax_errors" -ne 0 ]
then
    echo "Error: $syntax_errors syntax errors found, aborting commit."
    exit 1
fi