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.') |