bin/ansible-pull
672794f5
 #!/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/>.
cc365159
 #
788e11f1
 # ansible-pull is a script that runs ansible in local mode
cc365159
 # after checking out a playbooks directory from source repo.  There is an
788e11f1
 # 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
cc365159
 # location underneath the workdir and then searches the source repo
788e11f1
 # for playbooks in the following order, stopping at the first match:
 #
 # 1. $workdir/path/playbook.yml, if specified
92147aff
 # 2. $workdir/$fqdn.yml
 # 3. $workdir/$hostname.yml
 # 4. $workdir/local.yml
788e11f1
 #
cc365159
 # the source repo must contain at least one of these playbooks.
672794f5
 
 import os
60d3e9f3
 import shutil
84c9caa8
 import subprocess
672794f5
 import sys
788e11f1
 import datetime
0841ed47
 import socket
cc365159
 from ansible import utils
672794f5
 
cc365159
 DEFAULT_REPO_TYPE = 'git'
672794f5
 DEFAULT_PLAYBOOK = 'local.yml'
53207ddb
 PLAYBOOK_ERRORS = {1: 'File does not exist',
                     2: 'File is not readable'}
 
672794f5
 
84c9caa8
 def _run(cmd):
034e8f59
     print >>sys.stderr, "Running: '%s'" % cmd
84c9caa8
     cmd = subprocess.Popen(cmd, shell=True,
                            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     (out, err) = cmd.communicate()
     print out
     if cmd.returncode != 0:
034e8f59
         print >>sys.stderr, err
53207ddb
     return cmd.returncode, out
 
84c9caa8
 
034e8f59
 def try_playbook(path):
     if not os.path.exists(path):
         return 1
     if not os.access(path, os.R_OK):
         return 2
     return 0
 
53207ddb
 
034e8f59
 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:
4b067fa4
         fqdn = socket.getfqdn()
         hostpb = "%s/%s.yml" % (path, fqdn)
         shorthostpb = "%s/%s.yml" % (path, fqdn.split('.')[0])
034e8f59
         localpb = "%s/%s" % (path, DEFAULT_PLAYBOOK)
         errors = []
4b067fa4
         for pb in [hostpb, shorthostpb, localpb]:
034e8f59
             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
 
53207ddb
 
672794f5
 def main(args):
     """ Set up and run a local playbook """
034e8f59
     usage = "%prog [options] [playbook.yml]"
cc365159
     parser = utils.SortedOptParser(usage=usage)
60d3e9f3
     parser.add_option('--purge', default=False, action='store_true',
cc365159
                       help='purge checkout after playbook run')
53207ddb
     parser.add_option('-o', '--only-if-changed', dest='ifchanged', default=False, action='store_true',
c2988dfd
                       help='only run the playbook if the repository has been updated')
c8a5aabf
     parser.add_option('-f', '--force', dest='force', default=False,
                       action='store_true',
                       help='run the playbook even if the repository could '
                            'not be updated')
672794f5
     parser.add_option('-d', '--directory', dest='dest', default=None,
cc365159
                       help='directory to checkout repository to')
788e11f1
     parser.add_option('-U', '--url', dest='url', default=None,
cc365159
                       help='URL of the playbook repository')
84c9caa8
     parser.add_option('-C', '--checkout', dest='checkout',
cc365159
                       help='branch/tag/commit to checkout.  '
                       'Defaults to behavior of repository module.')
291fb9e9
     parser.add_option('-i', '--inventory-file', dest='inventory',
dc984d94
                       help="location of the inventory host file")
cc365159
     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)
672794f5
     options, args = parser.parse_args(args)
 
f8b23e57
     hostname = socket.getfqdn()
788e11f1
     if not options.dest:
f8b23e57
         # use a hostname dependent directory, in case of $HOME on nfs
         options.dest = utils.prepare_writeable_dir('~/.ansible/pull/%s' % hostname)
788e11f1
 
b37ecb05
     options.dest = os.path.abspath(options.dest)
 
788e11f1
     if not options.url:
cc365159
         parser.error("URL for repository not specified, use -h for help")
034e8f59
         return 1
788e11f1
 
     now = datetime.datetime.now()
034e8f59
     print >>sys.stderr, now.strftime("Starting ansible-pull at %F %T")
788e11f1
 
291fb9e9
     inv_opts = 'localhost,'
f8b23e57
     limit_opts = 'localhost:%s:127.0.0.1' % hostname
cc365159
     base_opts = '-c local --limit "%s"' % limit_opts
     repo_opts = "name=%s dest=%s" % (options.url, options.dest)
     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
291fb9e9
             )
53207ddb
     rc, out = _run(cmd)
2b24131b
     if rc != 0:
c8a5aabf
         if options.force:
51638df4
             print "Unable to update repository. Continuing with (forced) run of playbook."
c8a5aabf
         else:
             return rc
53207ddb
     elif options.ifchanged and '"changed": true' not in out:
         print "Repository has not changed, quitting."
         return 0
2b24131b
 
034e8f59
     playbook = select_playbook(options.dest, args)
 
     if playbook is None:
         print >>sys.stderr, "Could not find a playbook to run."
         return 1
788e11f1
 
cc365159
     cmd = 'ansible-playbook %s %s' % (base_opts, playbook)
291fb9e9
     if options.inventory:
         cmd += ' -i "%s"' % options.inventory
672794f5
     os.chdir(options.dest)
53207ddb
     rc, out = _run(cmd)
60d3e9f3
 
     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))
 
84c9caa8
     return rc
672794f5
 
 if __name__ == '__main__':
     try:
         sys.exit(main(sys.argv[1:]))
84c9caa8
     except KeyboardInterrupt, e:
         print >>sys.stderr, "Exit on user request.\n"
672794f5
         sys.exit(1)