From 3803b27f6cca3295c5b791bdcb59224c0b46f46f Mon Sep 17 00:00:00 2001 From: Stephen Fromm Date: Mon, 30 Apr 2012 14:24:58 -0700 Subject: [PATCH 1/4] Add playbook and template to set up ansible-pull Playbook will install ansible, create directory where git checkout goes, and set up a cron job to run ansible-pull. --- examples/playbooks/ansible_pull.yml | 19 +++++++++++++++++++ examples/playbooks/templates/ansible-pull.j2 | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 examples/playbooks/ansible_pull.yml create mode 100644 examples/playbooks/templates/ansible-pull.j2 diff --git a/examples/playbooks/ansible_pull.yml b/examples/playbooks/ansible_pull.yml new file mode 100644 index 0000000000..96b82735e7 --- /dev/null +++ b/examples/playbooks/ansible_pull.yml @@ -0,0 +1,19 @@ +--- +- hosts: all + user: root + vars: + # schdule is fed directly to cron + schedule: '*/15 * * * *' + # User to run ansible-pull as from cron + cron_user: root + # Directory to where repository will be cloned + workdir: /var/lib/ansible/local + # Repository to check out + repo_url: git://github.com/sfromm/ansible-playbooks.git + tasks: + - name: Install ansible + action: yum pkg=ansible state=installed + - name: Create local directory to work from + action: file path=$workdir state=directory owner=root group=root mode=0751 + - name: Create crontab entry to clone/pull git repository + action: template src=templates/ansible-pull.j2 dest=/etc/cron.d/ansible-pull owner=root group=root mode=0644 diff --git a/examples/playbooks/templates/ansible-pull.j2 b/examples/playbooks/templates/ansible-pull.j2 new file mode 100644 index 0000000000..bab3f3f73d --- /dev/null +++ b/examples/playbooks/templates/ansible-pull.j2 @@ -0,0 +1,2 @@ +# Cron job to git clone/pull a repo and then run locally +{{ schedule }} {{ cron_user }} ansible-pull -d {{ workdir }} -U {{ repo_url }} From 672794f586dd73b91100f6dba76f6dcb6e2c76c9 Mon Sep 17 00:00:00 2001 From: Stephen Fromm Date: Mon, 30 Apr 2012 14:27:57 -0700 Subject: [PATCH 2/4] Add ansible-pull A first stab at a pull-based model for ansible. This does two things: 1. Invoke the git module via Runner to set up a git repository on the localhost. It sets up Runner to use transport='local' and forces the inventory to just 'localhost'. 2. Run any playbooks provided. By default, this wants to run the playbook local.yml. This also sets transport='local' and sets the host_list to a list: localhost, fqdn, and hostname. The reason for setting the host_list and not using override_hosts is because there may be plays in the playbook that are not meant for a specific host. That is, if the git repository is for the entire site and not host-specific, you don't want to override hosts and apply all plays to any given host. This has the downside of potentially running a play three times if the play is defined for 'hosts: all'. --- bin/ansible-pull | 147 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100755 bin/ansible-pull diff --git a/bin/ansible-pull b/bin/ansible-pull new file mode 100755 index 0000000000..df7b5d17be --- /dev/null +++ b/bin/ansible-pull @@ -0,0 +1,147 @@ +#!/usr/bin/env python + +# (c) 2012, Stephen Fromm +# +# 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 . + +import os +import sys +import socket +import logging + +import ansible.playbook +import ansible.runner +import ansible.constants as C +from ansible import errors +from ansible import callbacks +from ansible import utils +from ansible import inventory + +DEFAULT_PLAYBOOK = 'local.yml' + +def main(args): + """ Set up and run a local playbook """ + usage = "%prog [options]" + parser = utils.base_parser(constants=C, usage=usage, + connect_opts=False, runas_opts=False) + parser.set_defaults(module_name='git', transport='local', + one_line=False, tree=None) + parser.add_option('-d', '--directory', dest='dest', default=None, + help='Directory to checkout git repository') + parser.add_option('-U', '--url', dest='url', default=None, + help='URL of git repository') + parser.add_option('-C', '--checkout', dest='checkout', default="HEAD", + help='Branch/Tag/Commit to checkout. Defaults to HEAD.') + parser.remove_option('-k') # Remove ssh password option + parser.remove_option('-K') # Remove sudo password option + parser.remove_option('-T') # Remove ssh timeout option + options, args = parser.parse_args(args) + + clirunner_cb = callbacks.CliRunnerCallbacks() + clirunner_cb.options = options + + # ---------------------------------------------- + # First git clone/pull + + git_opts = "repo=%s dest=%s version=%s" % (options.url, options.dest, options.checkout) + pattern = "localhost" + inventory_manager = inventory.Inventory([pattern]) + + """ + Ideally, changes should be reported via logging and not to STDOUT + """ + + runner = ansible.runner.Runner( + module_name=options.module_name, + module_args=git_opts, + module_path=options.module_path, + inventory=inventory_manager, + forks=options.forks, + pattern=pattern, + callbacks=clirunner_cb, + transport=options.transport, + debug=options.debug + ) + try: + runner.run() + except errors.AnsibleError, e: + print >>sys.stderr, "ERROR: %s" % e + return 1 + + # ---------------------------------------------- + # Second, run the playbook + """ + Change to the directory where the git checkout is located. + Insert 'local.yml' as the first playbook to be run. This + supports multiple playbooks being supplied on the CLI, similar + to ansible-playbook. This then loops on all the playbooks, + instantiates and runs a playbook. A couple things of note: + * The transport uses the default set above, local + * The host_list argument to Playbook is set to a list of + names. These are localhost, the fqdn, and hostname. + This last point is problematic because it will run a playbook + 3 times if the playbook is for 'all' hosts. We do not necessarily + want to override 'hosts' in the playbook because they may be generic + across the entire infrastructure -- not host specific. + + Finally, this should use the logging module in some manner and + not print data to STDOUT. + """ + + if os.path.exists("%s/%s" % (options.dest, DEFAULT_PLAYBOOK)): + args.insert(0, DEFAULT_PLAYBOOK) + os.chdir(options.dest) + hostname = socket.getfqdn() + stats = callbacks.AggregateStats() + playbook_cb = callbacks.PlaybookCallbacks() + pbrunner_cb = callbacks.PlaybookRunnerCallbacks(stats) + local_host = [pattern, hostname, hostname.split('.')[0]] + + for playbook in args: + pb = ansible.playbook.PlayBook( + playbook=playbook, + host_list=local_host, + module_path=options.module_path, + debug=options.debug, + runner_callbacks=pbrunner_cb, + callbacks=playbook_cb, + transport=options.transport, + stats=stats + ) + """ + This just takes the reporting from ansible-playbook. + Ideally, this should use logging to report success/failure/changes. + """ + try: + pb.run() + hosts = sorted(pb.stats.processed.keys()) + print "RECAP\n\n" + for h in hosts: + t = pb.stats.summarize(h) + print "%-30s : ok=%4s changed=%4s unreachable=%4s failed=%4s " % (h, + t['ok'], t['changed'], t['unreachable'], t['failures'] + ) + print "\n" + except errors.AnsibleError, e: + print >>sys.stderr, "ERROR: %s" % e + return 1 + + return 0 + +if __name__ == '__main__': + try: + sys.exit(main(sys.argv[1:])) + except errors.AnsibleError, e: + print >>sys.stderr, "ERROR: %s" % e + sys.exit(1) From 23ff967f38c88be199c61355a40dd7fbb67d0ead Mon Sep 17 00:00:00 2001 From: Stephen Fromm Date: Tue, 1 May 2012 22:49:17 -0700 Subject: [PATCH 3/4] Copy inventory file to client --- examples/playbooks/ansible_pull.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/playbooks/ansible_pull.yml b/examples/playbooks/ansible_pull.yml index 96b82735e7..8a7e61daf4 100644 --- a/examples/playbooks/ansible_pull.yml +++ b/examples/playbooks/ansible_pull.yml @@ -15,5 +15,8 @@ action: yum pkg=ansible state=installed - name: Create local directory to work from action: file path=$workdir state=directory owner=root group=root mode=0751 + - name: Copy ansible inventory file to client + action: copy src=/etc/ansible/hosts dest=/etc/ansible/hosts + owner=root group=root mode=0644 - name: Create crontab entry to clone/pull git repository action: template src=templates/ansible-pull.j2 dest=/etc/cron.d/ansible-pull owner=root group=root mode=0644 From 84c9caa80588b025669c0ff8dbb0a51ebe012293 Mon Sep 17 00:00:00 2001 From: Stephen Fromm Date: Wed, 2 May 2012 09:48:15 -0700 Subject: [PATCH 4/4] Simplify ansible-pull to just invoke ansible and ansible-playbook This eliminates the creation of runner and playbook instances and just invokes the processes ansible and ansible-playbook. --- bin/ansible-pull | 133 +++++++++-------------------------------------- 1 file changed, 25 insertions(+), 108 deletions(-) diff --git a/bin/ansible-pull b/bin/ansible-pull index df7b5d17be..b75c4631e4 100755 --- a/bin/ansible-pull +++ b/bin/ansible-pull @@ -16,132 +16,49 @@ # along with Ansible. If not, see . import os +import subprocess import sys -import socket -import logging - -import ansible.playbook -import ansible.runner -import ansible.constants as C -from ansible import errors -from ansible import callbacks -from ansible import utils -from ansible import inventory +from optparse import OptionParser DEFAULT_PLAYBOOK = 'local.yml' +def _run(cmd): + cmd = subprocess.Popen(cmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = cmd.communicate() + print out + if cmd.returncode != 0: + print err + return cmd.returncode + def main(args): """ Set up and run a local playbook """ usage = "%prog [options]" - parser = utils.base_parser(constants=C, usage=usage, - connect_opts=False, runas_opts=False) - parser.set_defaults(module_name='git', transport='local', - one_line=False, tree=None) + parser = OptionParser() parser.add_option('-d', '--directory', dest='dest', default=None, help='Directory to checkout git repository') - parser.add_option('-U', '--url', dest='url', default=None, + parser.add_option('-U', '--url', dest='url', + default=None, help='URL of git repository') - parser.add_option('-C', '--checkout', dest='checkout', default="HEAD", + parser.add_option('-C', '--checkout', dest='checkout', + default="HEAD", help='Branch/Tag/Commit to checkout. Defaults to HEAD.') - parser.remove_option('-k') # Remove ssh password option - parser.remove_option('-K') # Remove sudo password option - parser.remove_option('-T') # Remove ssh timeout option options, args = parser.parse_args(args) - clirunner_cb = callbacks.CliRunnerCallbacks() - clirunner_cb.options = options - - # ---------------------------------------------- - # First git clone/pull - git_opts = "repo=%s dest=%s version=%s" % (options.url, options.dest, options.checkout) - pattern = "localhost" - inventory_manager = inventory.Inventory([pattern]) - - """ - Ideally, changes should be reported via logging and not to STDOUT - """ + cmd = 'ansible all -c local -m git -a "%s"' % git_opts + rc = _run(cmd) + if rc != 0: + return rc - runner = ansible.runner.Runner( - module_name=options.module_name, - module_args=git_opts, - module_path=options.module_path, - inventory=inventory_manager, - forks=options.forks, - pattern=pattern, - callbacks=clirunner_cb, - transport=options.transport, - debug=options.debug - ) - try: - runner.run() - except errors.AnsibleError, e: - print >>sys.stderr, "ERROR: %s" % e - return 1 - - # ---------------------------------------------- - # Second, run the playbook - """ - Change to the directory where the git checkout is located. - Insert 'local.yml' as the first playbook to be run. This - supports multiple playbooks being supplied on the CLI, similar - to ansible-playbook. This then loops on all the playbooks, - instantiates and runs a playbook. A couple things of note: - * The transport uses the default set above, local - * The host_list argument to Playbook is set to a list of - names. These are localhost, the fqdn, and hostname. - This last point is problematic because it will run a playbook - 3 times if the playbook is for 'all' hosts. We do not necessarily - want to override 'hosts' in the playbook because they may be generic - across the entire infrastructure -- not host specific. - - Finally, this should use the logging module in some manner and - not print data to STDOUT. - """ - - if os.path.exists("%s/%s" % (options.dest, DEFAULT_PLAYBOOK)): - args.insert(0, DEFAULT_PLAYBOOK) os.chdir(options.dest) - hostname = socket.getfqdn() - stats = callbacks.AggregateStats() - playbook_cb = callbacks.PlaybookCallbacks() - pbrunner_cb = callbacks.PlaybookRunnerCallbacks(stats) - local_host = [pattern, hostname, hostname.split('.')[0]] - - for playbook in args: - pb = ansible.playbook.PlayBook( - playbook=playbook, - host_list=local_host, - module_path=options.module_path, - debug=options.debug, - runner_callbacks=pbrunner_cb, - callbacks=playbook_cb, - transport=options.transport, - stats=stats - ) - """ - This just takes the reporting from ansible-playbook. - Ideally, this should use logging to report success/failure/changes. - """ - try: - pb.run() - hosts = sorted(pb.stats.processed.keys()) - print "RECAP\n\n" - for h in hosts: - t = pb.stats.summarize(h) - print "%-30s : ok=%4s changed=%4s unreachable=%4s failed=%4s " % (h, - t['ok'], t['changed'], t['unreachable'], t['failures'] - ) - print "\n" - except errors.AnsibleError, e: - print >>sys.stderr, "ERROR: %s" % e - return 1 - - return 0 + cmd = 'ansible-playbook -c local %s' % DEFAULT_PLAYBOOK + rc = _run(cmd) + return rc if __name__ == '__main__': try: sys.exit(main(sys.argv[1:])) - except errors.AnsibleError, e: - print >>sys.stderr, "ERROR: %s" % e + except KeyboardInterrupt, e: + print >>sys.stderr, "Exit on user request.\n" sys.exit(1)