Puppet Version Control

Version 7 (Devon Peters, 08/02/2011 11:54 am)

1 1
# Keep your Puppet manifests under version control
2 1
3 1
It's a good idea to keep your Puppet manifests and other
4 1
configuration files under version control, for example Subversion
5 1
or CVS. To do this, just set up the Puppet Master the way you want,
6 1
and then import the whole of /etc/puppet into Subversion:
7 1
8 1
    $ svn import /etc/puppet https://www.your-svn-server-here.com/svn/puppet/trunk
9 1
10 5 Adam Wenner
Once you have this imported, you will need to remove the files from /etc/puppet 
11 5 Adam Wenner
and check it out from your SVN repo, to make it a working copy, so that
12 5 Adam Wenner
svn up /etc/puppet works.
13 1
14 5 Adam Wenner
    $ svn co https://www.your-svn-server-here.com/svn/puppet/trunk /etc/puppet
15 5 Adam Wenner
16 5 Adam Wenner
In order to edit your puppet config, you can then check out a working copy somewhere else:
17 5 Adam Wenner
18 1
    $ svn co https://www.your-svn-server-here.com/svn/puppet/trunk puppet
19 1
20 1
Edit it, then commit your changes, and update the master copy on
21 1
the server:
22 1
23 1
    $ svn up /etc/puppet
24 1
25 1
The Puppet master automatically detects that its configuration
26 1
files have changed.
27 1
28 1
Remember when you create new certificates, you are modifying the
29 1
Puppet master's working copy, so you need to commit these changes
30 1
every so often. This has the added benefit that if you should lose
31 1
the Puppet master server, you can easily recreate it by just
32 1
checking out a copy of the Puppet tree into /etc/puppet.
33 1
34 1
## Configuration Versioning
35 1
36 1
From version 0.25.0, a new configuration option, config\_version,
37 1
is now available:
38 1
39 1
    config_version = /usr/local/bin/return_version
40 1
41 1
The option allows you to specify a command that returns a version
42 1
for the configuration that is being applied to your hosts. The
43 1
command should return a string, such as a version number or name.
44 5 Adam Wenner
45 5 Adam Wenner
One such example of a command to return a version number would be:
46 5 Adam Wenner
47 6 Andreas Paul
    svn info /etc/puppet/ | grep Revision: | egrep -o [0-9]+
48 1
49 1
Puppet then runs this command at compile time. Each resource is
50 1
marked with the value returned from this command. This value is
51 1
also added to the log instance, serialised and sent along with any
52 1
report generated. This allows you to parse your report output and
53 1
ascertain which configuration version was used to generate the
54 1
resource.
55 1
56 1
# Using Hooks
57 1
58 1
Hooks let you extend the value of Subversion (or Git, CVS, etc) to
59 1
perform error checking, stage files and even produce audit trails.
60 1
61 1
## SVN Hooks
62 1
63 1
### SVN Pre-Commit Hook
64 1
65 1
To catch syntax errors and other basic problems, you can use a
66 1
Subversion pre-commit hook like this:
67 1
68 1
    #!/bin/sh
69 1
    # SVN pre-commit hook to check Puppet syntax for .pp files
70 1
    # Modified from http://mail.madstop.com/pipermail/puppet-users/2007-March/002034.html
71 1
    REPOS="$1"
72 1
    TXN="$2"
73 1
    tmpfile=`mktemp`
74 1
    export HOME=/
75 1
    SVNLOOK=/usr/bin/svnlook
76 4 Dan Carley
    $SVNLOOK changed -t "$TXN" "$REPOS" | awk '/^[^D].*\.pp$/ {print $2}' | while read line
77 1
    do
78 1
        $SVNLOOK cat -t "$TXN" "$REPOS" "$line" > $tmpfile
79 1
        if [ $? -ne 0 ]
80 1
        then
81 1
            echo "Warning: Failed to checkout $line" >&2
82 1
        fi
83 1
        puppet --color=false --confdir=/tmp --vardir=/tmp --parseonly --ignoreimport $tmpfile >&2
84 1
        if [ $? -ne 0 ]
85 1
        then
86 1
            echo "Puppet syntax error in $line." >&2
87 1
            exit 2
88 1
        fi
89 1
    done
90 1
    res=$?
91 1
    rm -f $tmpfile
92 1
    if [ $res -ne 0 ]
93 1
    then
94 1
        exit $res
95 1
    fi
96 1
97 1
If you get errors like this when committing:
98 1
99 1
    /usr/lib/ruby/site_ruby/1.8/puppet/defaults.rb:102:in `handle': private method `split' called for nil:NilClass (NoMethodError)
100 1
101 1
It's because the PATH environmental variable doesn't exist. It
102 1
doesn't matter what it is, but puppet wants it to be there. Try
103 1
adding PATH="" somewhere in the top of the pre-commit script.
104 1
105 1
### SVN Post-Commit Hook
106 1
107 1
Using a post-commit hook can be handy if you want your commits to
108 1
automatically be seen by Puppet, e.g. you don't have to do the last
109 1
step shown above (svn up). Also, by integrating
110 1
[cvsspam](http://blog.thinkphp.de/archives/239-Using-CVSSpam-with-Subversion.html)
111 1
you can provide an audit trail with nicely formatted, coloured
112 1
diffs.
113 1
114 1
Here's a simplified example of a post-commit that simply updates
115 1
the files (previous checked-out) in /etc/puppet.
116 1
117 1
    #!/bin/sh
118 1
    REPOS="$1"
119 1
    REV="$2"
120 1
    svn up /etc/puppet
121 1
122 1
Of course much more can be done here, but that is a nice start.
123 1
124 1
## Git Hooks
125 1
126 1
### Git Update Hook
127 1
128 1
To catch syntax errors and other basic problems, you can use a
129 1
server-side Git update hook like this:
130 1
131 1
    #!/bin/bash
132 1
    
133 1
    NOBOLD="\033[0m"
134 1
    BOLD="\033[1m"
135 1
    BLACK="\033[30m"
136 1
    GREY="\033[0m"
137 1
    RED="\033[31m"
138 1
    GREEN="\033[32m"
139 1
    YELLOW="\033[33m"
140 1
    BLUE="\033[34m"
141 1
    MAGENTA="\033[35m"
142 1
    CYAN="\033[36m"
143 1
    WHITE="\033[37m"
144 1
    
145 1
    # V +1007
146 1
    
147 1
    # Peff helped:
148 1
    # http://thread.gmane.org/gmane.comp.version-control.git/118626
149 1
    
150 1
    syntax_check="puppet --color=false --confdir=/tmp --vardir=/tmp --parseonly --ignoreimport"
151 1
    tmp=$(mktemp /tmp/git.update.XXXXXX)
152 1
    log=$(mktemp /tmp/git.update.log.XXXXXX)
153 1
    tree=$(mktemp /tmp/git.diff-tree.XXXXXX)
154 1
    
155 1
    git diff-tree -r "$2" "$3" > $tree
156 1
    
157 1
    echo
158 1
    echo diff-tree:
159 1
    cat $tree
160 1
    
161 1
    exit_status=0
162 1
    
163 1
    while read old_mode new_mode old_sha1 new_sha1 status name
164 1
    do
165 1
      # skip lines showing parent commit
166 1
      test -z "$new_sha1" && continue
167 1
      # skip deletions
168 1
      [ "$new_sha1" = "0000000000000000000000000000000000000000" ] && continue
169 1
      # Only test .pp files
170 1
      if [[ $name =~ [.]pp$ ]]
171 1
      then
172 1
        git cat-file blob $new_sha1 > $tmp
173 1
        set -o pipefail
174 1
        $syntax_check $tmp 2>&1 | sed 's|/tmp/git.update.......:\([0-9]*\)$|JOJOMOJO:\1|'> $log
175 1
        if [[ $? != 0 ]]
176 1
        then
177 1
          echo
178 1
          echo -e "$(cat $log | sed 's|JOJOMOJO|'\\${RED}${name}\\${NOBOLD}'|')" >&2
179 1
          echo -e "For more details run this: ${CYAN} git diff $old_sha1 $new_sha1 ${NOBOLD}" >&2 
180 1
          echo
181 1
          exit_status=1
182 1
        fi
183 1
      fi
184 1
    done < $tree
185 1
    
186 1
    rm -f $log $tmp $tree
187 1
    exit $exit_status
188 1
189 1
### Git Pre-Commit Hook
190 1
191 1
To catch syntax errors and other basic problems, you can use a
192 1
client-side Git pre-commit hook like this:
193 1
194 1
    #!/bin/sh
195 1
    
196 1
    syntax_errors=0
197 1
    error_msg=$(mktemp /tmp/error_msg.XXXXXX)
198 1
    
199 1
    if git rev-parse --quiet --verify HEAD > /dev/null
200 1
    then
201 1
        against=HEAD
202 1
    else
203 1
        # Initial commit: diff against an empty tree object
204 1
        against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
205 1
    fi
206 1
    
207 1
    # Get list of new/modified manifest and template files to check (in git index)
208 1
    for indexfile in `git diff-index --diff-filter=AM --name-only --cached $against | egrep '\.(pp|erb)'`
209 1
    do
210 1
        # Don't check empty files
211 1
        if [ `git cat-file -s :0:$indexfile` -gt 0 ]
212 1
        then
213 1
            case $indexfile in
214 1
                *.pp )
215 1
                    # Check puppet manifest syntax
216 1
                    git cat-file blob :0:$indexfile | puppet --color=false --parseonly --ignoreimport > $error_msg ;;
217 1
                *.erb )
218 1
                    # Check ERB template syntax
219 1
                    git cat-file blob :0:$indexfile | erb -x -T - | ruby -c 2> $error_msg > /dev/null ;;
220 1
            esac
221 1
            if [ "$?" -ne 0 ]
222 1
            then
223 1
                echo -n "$indexfile: "
224 1
                cat $error_msg
225 1
                syntax_errors=`expr $syntax_errors + 1`
226 1
            fi
227 1
        fi
228 1
    done
229 1
    
230 1
    rm -f $error_msg
231 1
    
232 1
    if [ "$syntax_errors" -ne 0 ]
233 1
    then
234 1
        echo "Error: $syntax_errors syntax errors found, aborting commit."
235 1
        exit 1
236 1
    fi
237 7 Devon Peters
238 7 Devon Peters
239 7 Devon Peters
## Bazaar (bzr) Hooks
240 7 Devon Peters
241 7 Devon Peters
### Bazaar Pre-Commit Hook
242 7 Devon Peters
243 7 Devon Peters
This is a client side pre-commit hook to catch basic syntax errors.
244 7 Devon Peters
245 7 Devon Peters
    #!/usr/bin/env python
246 7 Devon Peters
    #
247 7 Devon Peters
    # BZR pre-commit hook, which will run some basic syntax checking on manifests
248 7 Devon Peters
    # in the current branch.
249 7 Devon Peters
    #
250 7 Devon Peters
    # To use this script, place it in your ~/.bazzar/plugins directory (create
251 7 Devon Peters
    # this directory if it doesn't exist).
252 7 Devon Peters
    #
253 7 Devon Peters
    
254 7 Devon Peters
    from bzrlib import branch
255 7 Devon Peters
    import os
256 7 Devon Peters
    import sys
257 7 Devon Peters
    import subprocess
258 7 Devon Peters
    
259 7 Devon Peters
    def get_branch_root(directory):
260 7 Devon Peters
        """Find the root directory of the current BZR branch."""
261 7 Devon Peters
        while os.path.exists(directory):
262 7 Devon Peters
            if os.path.exists(os.path.join(directory, '.bzr')):
263 7 Devon Peters
                return directory
264 7 Devon Peters
            if directory == '/':
265 7 Devon Peters
                break
266 7 Devon Peters
            (parent, dir) = os.path.split(directory)
267 7 Devon Peters
            directory = parent
268 7 Devon Peters
        print "Commit FAILED:  Can't locate BZR Root."
269 7 Devon Peters
        sys.exit(1)
270 7 Devon Peters
    
271 7 Devon Peters
    def check_puppet_syntax(local, master, old_revno, old_revid, future_revno,
272 7 Devon Peters
                           future_revid, tree_delta, future_tree):
273 7 Devon Peters
        """This will run some basic syntax checking on the puppet manifests."""
274 7 Devon Peters
                
275 7 Devon Peters
        # Check syntax on changed files
276 7 Devon Peters
        errors = []
277 7 Devon Peters
        os.chdir(get_branch_root(os.getcwd()))
278 7 Devon Peters
        print "\n" # make some space so we aren't clobbered by bzr's status msgs
279 7 Devon Peters
        for file in tree_delta.added + tree_delta.renamed + tree_delta.modified:
280 7 Devon Peters
            file = file[0]
281 7 Devon Peters
            if file.endswith(".pp"):
282 7 Devon Peters
                print "Checking syntax in: %s" % (file)
283 7 Devon Peters
                try:
284 7 Devon Peters
                    process = subprocess.Popen(["puppet", "--parseonly", file],
285 7 Devon Peters
                        stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
286 7 Devon Peters
                    process.wait()
287 7 Devon Peters
                    if process.returncode != 0:
288 7 Devon Peters
                        errors.append((file, ''.join(process.stdout.readlines())))
289 7 Devon Peters
                except OSError, e:
290 7 Devon Peters
                    print "\n\n Error: failed to execute 'puppet': %s" % (e)
291 7 Devon Peters
                    sys.exit(1)
292 7 Devon Peters
        if errors:
293 7 Devon Peters
            print "\nSyntax errors were found:\n"
294 7 Devon Peters
            for error in errors:
295 7 Devon Peters
                print "%s: %s" % (error[0], error[1]),
296 7 Devon Peters
            print "\nCommit FAILED"
297 7 Devon Peters
            sys.exit(1)
298 7 Devon Peters
        else:
299 7 Devon Peters
            print "\nAll syntax checks PASSED"
300 7 Devon Peters
    
301 7 Devon Peters
    
302 7 Devon Peters
    # This is where the magic happens
303 7 Devon Peters
    branch.Branch.hooks.install_named_hook('pre_commit', check_puppet_syntax,
304 7 Devon Peters
                                           'Check puppet manifests for syntax errors.')