diff --git a/README.md b/README.md index b120d82660..409c3d0f0b 100644 --- a/README.md +++ b/README.md @@ -36,16 +36,21 @@ Design Principles Requirements ============ -For the server the tool is running from, *only*: +Requirements are extremely minimal. + +If you are running python 2.6 on the 'overlord' machine, you will need: * paramiko - * python 2.6 (or the 2.4/2.5 backport of the multiprocessing module) - * PyYAML (only if using playbooks) + * PyYAML (if using playbooks) -Optional -- If you want to push templates, the nodes need a template library, -which for bonus points you can install with ansible! Easy enough. +If you are running less than Python 2.6, you will also need - * python-jinja2 + * the Python 2.4 or 2.5 backport of the multiprocessing module + * simplejson + +On the managed nodes, to use templating, you will need: + + * python-jinja2 (you can install this with ansible) Patterns and Groups =================== diff --git a/bin/ansible b/bin/ansible index fb87f3fd76..04d2c45366 100755 --- a/bin/ansible +++ b/bin/ansible @@ -21,7 +21,6 @@ try: import json except ImportError: import simplejson as json - from optparse import OptionParser import sys import os @@ -31,13 +30,19 @@ import ansible.runner import ansible.playbook import ansible.constants as C from optparse import OptionParser +from ansible.utils import * class Cli(object): + + # ---------------------------------------------- def __init__(self): pass - def runner(self): + # ---------------------------------------------- + + def parse(self): + parser = OptionParser(usage = 'ansible [options]') parser.add_option("-a", "--args", dest="module_args", help="module arguments", default=C.DEFAULT_MODULE_ARGS) @@ -60,19 +65,22 @@ class Cli(object): parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user', help='connect as this user') options, args = parser.parse_args() - + if len(args) == 0 or len(args) > 1: parser.print_help() sys.exit(1) - pattern = args[0] + return (options, args) + + # ---------------------------------------------- + + def run(self, options, args): + pattern = args[0] sshpass = None if options.ask_pass: sshpass = getpass.getpass(prompt="SSH password: ") - self.options = options - - runner = ansible.runner.Runner( + return ansible.runner.Runner( module_name=options.module_name, module_path=options.module_path, module_args=shlex.split(options.module_args), @@ -83,108 +91,38 @@ class Cli(object): forks=options.forks, pattern=pattern, verbose=True, - ) - return runner + ).run() - def output(self, results): + # ---------------------------------------------- - # if specifying output destination (aka tree output saves), create the - # directory to output to + def output(self, results, options, args): if results is None: - print >> sys.stderr, "No hosts matched" - sys.exit(1) - - options = self.options - - # TODO: split into function + exit("No hosts matched") if options.tree: - if options.tree[0] != '/': - options.tree = os.path.realpath(os.path.expanduser(options.tree)) - if not os.path.exists(options.tree): - try: - os.makedirs(options.tree) - except (IOError, OSError), e: - print >> sys.stderr, "Could not make dir %s: %s" % (options.tree, e) - sys.exit(1) - if not os.access(options.tree, os.W_OK): - print >> sys.stderr, "Cannot write to path %s" % options.tree - sys.exit(1) + prepare_writeable_dir(options.tree) - # now walk results and print output - - module_name = self.options.module_name - - for hostname in sorted(results['contacted']): - result = results['contacted'][hostname] + buf = '' + for hostname in contacted_hosts(results): + msg = host_report_msg( + hostname, + options.module_name, + contacted_host_result(results, hostname), + options.one_line + ) + if options.tree: + write_tree_file(hostname, msg) + buf += msg - # TODO: refactor - rc = 0 - msg = '' - failed = False - stdout = None - stderr = None - traceback = None - if type(result) == dict: - failed = result.get('failed', 0) - msg = result.get('msg', '') - if module_name == 'command': - # TODO: refactor - rc = result.get('rc',0) - stdout = result.get('stdout', '') - stderr = result.get('stderr', '') - traceback = result.get('traceback', '') + if has_dark_hosts(results): + buf += dark_hosts_msg(results) - if options.one_line: - # try to print everything on one line, but don't strip newlines - # if the command output happend to be too long - if module_name == 'command': - if not failed: - buf = "(stdout) %s" % stdout - if stderr.rstrip() != '': - buf = "(stdout) %s (stderr) %s" % (stdout,stderr) - print "%s | rc=%s | %s" % ( - hostname, rc, buf - ) - else: - print "%s | (error) %s" % (hostname, msg) - else: - print "%s | %s" % (hostname, result) - else: - # summarize response from command in multiple lines - buf = '' - if module_name == 'command': - if not failed: - buf += "%s | rc=%s >>\n" % (hostname, rc) - else: - buf += "%s | rc=%s | FAILED >>\n" % (hostname, rc) - buf += stdout - if stderr: - buf += stderr - if msg: - buf += msg - print buf - else: - if not failed: - buf += "%s >>\n" % hostname - else: - buf += "%s | FAILED >>\n" % hostname - buf += json.dumps(result, sort_keys=True) - print buf - if options.tree: - path = os.path.join(options.tree, hostname) - fd = open(path, "w+") - fd.write(buf) - fd.close() - - if len(results['dark'].keys()) > 0: - print >> sys.stderr, "*** Hosts which could not be contacted or did not respond: ***" - failed_hosts = results['dark'].keys() - for hostname in failed_hosts: - print >> sys.stderr, "%s:\n%s\n" % (hostname, results['dark'][hostname]) - print "" + print buf if __name__ == '__main__': cli = Cli() - cli.output(cli.runner().run()) + (options, args) = cli.parse() + results = cli.run(options, args) + cli.output(results, options, args) + diff --git a/lib/ansible/utils.py b/lib/ansible/utils.py new file mode 100755 index 0000000000..dbe8975054 --- /dev/null +++ b/lib/ansible/utils.py @@ -0,0 +1,135 @@ +# (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 +try: + import json +except ImportError: + import simplejson as json + +def err(msg): + print >> sys.stderr, msg + +def exit(msg, rc=1): + err(msg) + sys.exit(rc) + +def _bigjson(result): + return json.dumps(result, sort_keys=True, indent=4) + +def _json(result): + return json.dumps(result, sort_keys=True) + +def regular_generic_msg(hostname, result, oneline, caption): + if not oneline: + return "%s | %s >>\n%s" % (hostname, caption, _bigjson(result)) + else: + return "%s | %s >> %s" % (hostname, caption, _json(result)) + +def regular_success_msg(hostname, result, oneline): + return regular_generic_msg(hostname, result, oneline, 'success') + +def regular_failure_msg(hostname, result, oneline): + return regular_generic_msg(hostname, result, oneline, 'FAILED') + +def command_generic_msg(hostname, result, oneline, caption): + rc = result.get('rc', '0') + stdout = result.get('stdout','') + stderr = result.get('stderr', '') + msg = result.get('msg', '') + if not oneline: + buf = "%s | %s | rc=%s >>\n" % (hostname, caption, result.get('rc',0)) + if stdout: + buf += stdout + if stderr: + buf += stderr + if msg: + buf += msg + return buf + else: + if stderr: + return "%s | %s | rc=%s | (stdout) %s (stderr) %s" % (hostname, caption, rc, stdout, stderr) + else: + return "%s | %s | rc=%s | (stdout) %s" % (hostname, caption, rc, stdout) + +def command_success_msg(hostname, result, oneline): + return command_generic_msg(hostname, result, oneline, 'success') + +def command_failure_msg(hostname, result, oneline): + return command_generic_msg(hostname, result, oneline, 'FAILED') + +def write_tree_file(hostname,buf): + path = os.path.join(options.tree, hostname) + fd = open(path, "w+") + fd.write(buf) + fd.close() + +def is_failed(result): + failed = False + rc = 0 + if type(result) == dict: + failed = result.get('failed', 0) + rc = result.get('rc', 0) + if rc != 0: + return True + return failed + +def host_report_msg(hostname, module_name, result, oneline): + buf = '' + failed = is_failed(result) + if module_name == 'command': + if not failed: + buf = command_success_msg(hostname, result, oneline) + else: + buf = command_failure_msg(hostname, result, oneline) + else: + if not failed: + buf = regular_success_msg(hostname, result, oneline) + else: + buf = regular_failure_msg(hostname, result, oneline) + return buf + +def dark_hosts_msg(results): + buf = '' + if len(results['dark'].keys()) > 0: + buf += "*** Hosts which could not be contacted or did not respond: ***" + for hostname in results['dark'].keys(): + buf += "%s:\n%s\n" % (hostname, results['dark'][hostname]) + buf += "\n" + return buf + +def has_dark_hosts(results): + return len(results['dark'].keys()) > 0 + +def contacted_hosts(results): + return sorted(results['contacted']) + +def contacted_host_result(results, hostname): + return results['contacted'][hostname] + +def prepare_writeable_dir(tree): + if tree != '/': + tree = os.path.realpath(os.path.expanduser(options.tree)) + if not os.path.exists(tree): + try: + os.makedirs(tree) + except (IOError, OSError), e: + exit("Could not make dir %s: %s" % (tree, e)) + if not os.access(tree, os.W_OK): + exit("Cannot write to path %s" % tree) + + diff --git a/library/facter b/library/facter index 16bbbbe93e..a9cbf3006a 100755 --- a/library/facter +++ b/library/facter @@ -1,4 +1,4 @@ -#!/usr/bin/bash +#!/bin/bash # (c) 2012, Michael DeHaan #