ansible-pull 8.53 KB
Newer Older
Stephen Fromm committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#!/usr/bin/env python

# (c) 2012, Stephen Fromm <sfromm@gmail.com>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17
#
18
# ansible-pull is a script that runs ansible in local mode
19
# after checking out a playbooks directory from source repo.  There is an
20 21 22 23 24 25 26 27 28 29 30
# example playbook to bootstrap this script in the examples/ dir which
# installs ansible and sets it up to run on cron.

# usage:
#   ansible-pull -d /var/lib/ansible \
#                -U http://example.net/content.git [-C production] \
#                [path/playbook.yml]
#
# the -d and -U arguments are required; the -C argument is optional.
#
# ansible-pull accepts an optional argument to specify a playbook
31
# location underneath the workdir and then searches the source repo
32 33 34
# for playbooks in the following order, stopping at the first match:
#
# 1. $workdir/path/playbook.yml, if specified
35 36 37
# 2. $workdir/$fqdn.yml
# 3. $workdir/$hostname.yml
# 4. $workdir/local.yml
38
#
39
# the source repo must contain at least one of these playbooks.
Stephen Fromm committed
40 41

import os
42
import shutil
43
import subprocess
Stephen Fromm committed
44
import sys
45
import datetime
46
import socket
47 48
import random
import time
49
from ansible import utils
50
from ansible.utils import cmd_functions
51
from ansible import errors
Stephen Fromm committed
52

53
DEFAULT_REPO_TYPE = 'git'
Stephen Fromm committed
54
DEFAULT_PLAYBOOK = 'local.yml'
55 56 57
PLAYBOOK_ERRORS = {1: 'File does not exist',
                    2: 'File is not readable'}

58
VERBOSITY=0
Stephen Fromm committed
59

60 61 62
def increment_debug(option, opt, value, parser):
    global VERBOSITY
    VERBOSITY += 1
63

64 65 66 67 68 69 70
def try_playbook(path):
    if not os.path.exists(path):
        return 1
    if not os.access(path, os.R_OK):
        return 2
    return 0

71

72 73 74 75 76 77 78 79 80 81
def select_playbook(path, args):
    playbook = None
    if len(args) > 0 and args[0] is not None:
        playbook = "%s/%s" % (path, args[0])
        rc = try_playbook(playbook)
        if rc != 0:
            print >>sys.stderr, "%s: %s" % (playbook, PLAYBOOK_ERRORS[rc])
            return None
        return playbook
    else:
82 83 84
        fqdn = socket.getfqdn()
        hostpb = "%s/%s.yml" % (path, fqdn)
        shorthostpb = "%s/%s.yml" % (path, fqdn.split('.')[0])
85 86
        localpb = "%s/%s" % (path, DEFAULT_PLAYBOOK)
        errors = []
87
        for pb in [hostpb, shorthostpb, localpb]:
88 89 90 91 92 93 94 95 96 97
            rc = try_playbook(pb)
            if rc == 0:
                playbook = pb
                break
            else:
                errors.append("%s: %s" % (pb, PLAYBOOK_ERRORS[rc]))
        if playbook is None:
            print >>sys.stderr, "\n".join(errors)
        return playbook

98

Stephen Fromm committed
99 100
def main(args):
    """ Set up and run a local playbook """
101
    usage = "%prog [options] [playbook.yml]"
102
    parser = utils.SortedOptParser(usage=usage)
103
    parser.add_option('--purge', default=False, action='store_true',
104
                      help='purge checkout after playbook run')
105
    parser.add_option('-o', '--only-if-changed', dest='ifchanged', default=False, action='store_true',
106
                      help='only run the playbook if the repository has been updated')
107 108
    parser.add_option('-s', '--sleep', dest='sleep', default=None,
                      help='sleep for random interval (between 0 and n number of seconds) before starting. this is a useful way to disperse git requests')
109 110 111 112
    parser.add_option('-f', '--force', dest='force', default=False,
                      action='store_true',
                      help='run the playbook even if the repository could '
                           'not be updated')
Stephen Fromm committed
113
    parser.add_option('-d', '--directory', dest='dest', default=None,
114
                      help='directory to checkout repository to')
115 116
    #parser.add_option('-l', '--live', default=True, action='store_live',
    #                  help='Print the ansible-playbook output while running')
117
    parser.add_option('-U', '--url', dest='url', default=None,
118
                      help='URL of the playbook repository')
119
    parser.add_option('-C', '--checkout', dest='checkout',
120 121
                      help='branch/tag/commit to checkout.  '
                      'Defaults to behavior of repository module.')
122
    parser.add_option('-i', '--inventory-file', dest='inventory',
123
                      help="location of the inventory host file")
124 125
    parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append",
                      help="set additional variables as key=value or YAML/JSON", default=[])
126 127 128
    parser.add_option('-v', '--verbose', default=False, action="callback",
                      callback=increment_debug,
                      help='Pass -vvvv to ansible-playbook')
129 130 131 132
    parser.add_option('-m', '--module-name', dest='module_name',
                      default=DEFAULT_REPO_TYPE,
                      help='Module name used to check out repository.  '
                      'Default is %s.' % DEFAULT_REPO_TYPE)
133 134
    parser.add_option('--vault-password-file', dest='vault_password_file',
                    help="vault password file")
135 136
    parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
                      help='ask for sudo password')
Stephen Fromm committed
137 138
    options, args = parser.parse_args(args)

139
    hostname = socket.getfqdn()
140
    if not options.dest:
141 142
        # use a hostname dependent directory, in case of $HOME on nfs
        options.dest = utils.prepare_writeable_dir('~/.ansible/pull/%s' % hostname)
143

144 145
    options.dest = os.path.abspath(options.dest)

146
    if not options.url:
147
        parser.error("URL for repository not specified, use -h for help")
148
        return 1
149 150

    now = datetime.datetime.now()
151
    print >>sys.stderr, now.strftime("Starting ansible-pull at %F %T")
152

153 154 155 156
    if not options.inventory:
        inv_opts = 'localhost,'
    else:
        inv_opts = options.inventory
157
    limit_opts = 'localhost:%s:127.0.0.1' % hostname
158
    repo_opts = "name=%s dest=%s" % (options.url, options.dest)
159 160 161 162 163 164 165

    if VERBOSITY == 0:
        base_opts = '-c local --limit "%s"' % limit_opts
    elif VERBOSITY > 0:
        debug_level = ''.join([ "v" for x in range(0, VERBOSITY) ])
        base_opts = '-%s -c local --limit "%s"' % (debug_level, limit_opts)

166 167 168 169 170 171 172 173
    if options.checkout:
        repo_opts += ' version=%s' % options.checkout
    path = utils.plugins.module_finder.find_plugin(options.module_name)
    if path is None:
        sys.stderr.write("module '%s' not found.\n" % options.module_name)
        return 1
    cmd = 'ansible all -i "%s" %s -m %s -a "%s"' % (
            inv_opts, base_opts, options.module_name, repo_opts
174
            )
175

176 177 178 179 180 181 182 183 184 185 186 187
    if options.sleep:
        try:
            secs = random.randint(0,int(options.sleep));
        except ValueError:
            parser.error("%s is not a number." % options.sleep)
            return 1

        print >>sys.stderr, "Sleeping for %d seconds..." % secs
        time.sleep(secs);


    # RUN THe CHECKOUT COMMAND
188 189
    rc, out, err = cmd_functions.run_cmd(cmd, live=True)

190
    if rc != 0:
191
        if options.force:
192
            print "Unable to update repository. Continuing with (forced) run of playbook."
193 194
        else:
            return rc
195 196 197
    elif options.ifchanged and '"changed": true' not in out:
        print "Repository has not changed, quitting."
        return 0
198

199 200 201 202 203
    playbook = select_playbook(options.dest, args)

    if playbook is None:
        print >>sys.stderr, "Could not find a playbook to run."
        return 1
204

205
    cmd = 'ansible-playbook %s %s' % (base_opts, playbook)
206 207
    if options.vault_password_file:
        cmd += " --vault-password-file=%s" % options.vault_password_file
208 209
    if options.inventory:
        cmd += ' -i "%s"' % options.inventory
210 211
    for ev in options.extra_vars:
        cmd += ' -e "%s"' % ev
212 213
    if options.ask_sudo_pass:
        cmd += ' -K'
Stephen Fromm committed
214
    os.chdir(options.dest)
215 216 217

    # RUN THE PLAYBOOK COMMAND
    rc, out, err = cmd_functions.run_cmd(cmd, live=True)
218 219 220 221 222 223 224 225

    if options.purge:
        os.chdir('/')
        try:
            shutil.rmtree(options.dest)
        except Exception, e:
            print >>sys.stderr, "Failed to remove %s: %s" % (options.dest, str(e))

226
    return rc
Stephen Fromm committed
227 228 229 230

if __name__ == '__main__':
    try:
        sys.exit(main(sys.argv[1:]))
231 232
    except KeyboardInterrupt, e:
        print >>sys.stderr, "Exit on user request.\n"
Stephen Fromm committed
233
        sys.exit(1)