#!/usr/bin/env python # (c) 2012, Michael DeHaan # # This file is part of Ansible # # 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 sys import getpass import shlex import time from optparse import OptionParser import ansible.runner import ansible.constants as C from ansible import utils from ansible import errors from ansible import callbacks ######################################################## class Cli(object): ''' code behind bin/ansible ''' # ---------------------------------------------- def __init__(self): self.stats = callbacks.AggregateStats() self.callbacks = callbacks.CliRunnerCallbacks() self.silent_callbacks = callbacks.DefaultRunnerCallbacks() # ---------------------------------------------- def parse(self): ''' create an options parser for bin/ansible ''' parser = OptionParser(usage = 'ansible [options]') parser.add_option('-a', '--args', dest='module_args', help="module arguments", default=C.DEFAULT_MODULE_ARGS) parser.add_option('-B', '--background', dest='seconds', type='int', default=0, help='run asynchronously, failing after X seconds') parser.add_option('-f','--forks', dest='forks', default=C.DEFAULT_FORKS, type='int', help='number of parallel processes to use') parser.add_option('-i', '--inventory-file', dest='inventory', help='inventory host file', default=C.DEFAULT_HOST_LIST) parser.add_option('-k', '--ask-pass', default=False, action='store_true', help='ask for SSH password') parser.add_option('-M', '--module-path', dest='module_path', help="path to module library", default=C.DEFAULT_MODULE_PATH) parser.add_option('-m', '--module-name', dest='module_name', help="module name to execute", default=C.DEFAULT_MODULE_NAME) parser.add_option('-o', '--one-line', dest='one_line', action='store_true', help='condense output') parser.add_option('-P', '--poll', default=C.DEFAULT_POLL_INTERVAL, type='int', dest='poll_interval', help='set the poll interval if using -B') parser.add_option('-t', '--tree', dest='tree', default=None, help='log output to this directory') parser.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int', dest='timeout', help='set the SSH timeout in seconds') parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user', help='connect as this user') parser.add_option('-p', '--port', default=C.DEFAULT_REMOTE_PORT, type='int', dest='remote_port', help='set the remote ssh port') options, args = parser.parse_args() self.callbacks.options = options if len(args) == 0 or len(args) > 1: parser.print_help() sys.exit(1) return (options, args) # ---------------------------------------------- def run(self, options, args): ''' use Runner lib to do SSH things ''' pattern = args[0] sshpass = None if options.ask_pass: sshpass = getpass.getpass(prompt="SSH password: ") if options.tree: utils.prepare_writeable_dir(options.tree) if options.seconds: print "background launch...\n\n" runner = ansible.runner.Runner( module_name=options.module_name, module_path=options.module_path, module_args=shlex.split(options.module_args), remote_user=options.remote_user, remote_pass=sshpass, host_list=options.inventory, timeout=options.timeout, remote_port=options.remote_port, forks=options.forks, background=options.seconds, pattern=pattern, callbacks=self.callbacks, verbose=True, ) return (runner, runner.run()) # ---------------------------------------------- def get_polling_runner(self, old_runner, hosts, jid): return ansible.runner.Runner( module_name='async_status', module_path=old_runner.module_path, module_args=[ "jid=%s" % jid ], remote_user=old_runner.remote_user, remote_pass=old_runner.remote_pass, host_list=hosts, timeout=old_runner.timeout, forks=old_runner.forks, remote_port=old_runner.remote_port, pattern='*', callbacks=self.silent_callbacks, verbose=True, ) # ---------------------------------------------- def hosts_to_poll(self, results): hosts = [] for (host, res) in results['contacted'].iteritems(): if res.get('started',False): hosts.append(host) return hosts # ---------------------------------------------- def poll_if_needed(self, runner, results, options, args): ''' summarize results from Runner ''' if results is None: exit("No hosts matched") # BACKGROUND POLL LOGIC when -B and -P are specified # FIXME: refactor if options.seconds and options.poll_interval > 0: poll_hosts = results['contacted'].keys() if len(poll_hosts) == 0: exit("no jobs were launched successfully") ahost = poll_hosts[0] jid = results['contacted'][ahost].get('ansible_job_id', None) if jid is None: exit("unexpected error: unable to determine jid") clock = options.seconds while (clock >= 0): polling_runner = self.get_polling_runner(runner, poll_hosts, jid) poll_results = polling_runner.run() if poll_results is None: break for (host, host_result) in poll_results['contacted'].iteritems(): # override last result with current status result for report results['contacted'][host] = host_result print utils.async_poll_status(jid, host, clock, host_result) for (host, host_result) in poll_results['dark'].iteritems(): print "FAILED: %s => %s" % (host, host_result) clock = clock - options.poll_interval time.sleep(options.poll_interval) poll_hosts = self.hosts_to_poll(poll_results) ######################################################## if __name__ == '__main__': cli = Cli() (options, args) = cli.parse() try: (runner, results) = cli.run(options, args) except errors.AnsibleError as e: # Generic handler for ansible specific errors print "ERROR: %s" % str(e) sys.exit(1) else: cli.poll_if_needed(runner, results, options, args)