diff --git a/bin/ansible b/bin/ansible index f1d96e32d15..9d12a13a7f3 100755 --- a/bin/ansible +++ b/bin/ansible @@ -45,8 +45,18 @@ class Cli(object): def parse(self): ''' create an options parser for bin/ansible ''' - parser = utils.base_parser(constants=C, runas_opts=True, subset_opts=True, async_opts=True, - output_opts=True, connect_opts=True, check_opts=True, usage='%prog [options]') + parser = utils.base_parser( + constants=C, + runas_opts=True, + subset_opts=True, + async_opts=True, + output_opts=True, + connect_opts=True, + check_opts=True, + diff_opts=False, + usage='%prog [options]' + ) + parser.add_option('-a', '--args', dest='module_args', help="module arguments", default=C.DEFAULT_MODULE_ARGS) parser.add_option('-m', '--module-name', dest='module_name', @@ -54,6 +64,7 @@ class Cli(object): default=C.DEFAULT_MODULE_NAME) parser.add_option('--list-hosts', dest='listhosts', action='store_true', help="dump out a list of hosts matching input pattern, does not execute any modules!") + options, args = parser.parse_args() self.callbacks.options = options @@ -112,7 +123,8 @@ class Cli(object): callbacks=self.callbacks, sudo=options.sudo, sudo_pass=sudopass,sudo_user=options.sudo_user, transport=options.connection, subset=options.subset, - check=options.check + check=options.check, + diff=options.check ) if options.seconds: diff --git a/bin/ansible-playbook b/bin/ansible-playbook index 8a2f3595038..1b531ada7a0 100755 --- a/bin/ansible-playbook +++ b/bin/ansible-playbook @@ -52,8 +52,15 @@ def main(args): # create parser for CLI options usage = "%prog playbook.yml" - parser = utils.base_parser(constants=C, usage=usage, connect_opts=True, - runas_opts=True, subset_opts=True, check_opts=True) + parser = utils.base_parser( + constants=C, + usage=usage, + connect_opts=True, + runas_opts=True, + subset_opts=True, + check_opts=True, + diff_opts=True + ) parser.add_option('-e', '--extra-vars', dest="extra_vars", default=None, help="set additional key=value variables from the CLI") parser.add_option('-t', '--tags', dest='tags', default='all', @@ -122,7 +129,8 @@ def main(args): extra_vars=extra_vars, private_key_file=options.private_key_file, only_tags=only_tags, - check=options.check + check=options.check, + diff=options.diff ) if options.listhosts: diff --git a/lib/ansible/callbacks.py b/lib/ansible/callbacks.py index 619613fb333..abf3abdc295 100644 --- a/lib/ansible/callbacks.py +++ b/lib/ansible/callbacks.py @@ -1,4 +1,4 @@ -# (C) 2012, Michael DeHaan, +# (C) 2012-2013, Michael DeHaan, # This file is part of Ansible # @@ -35,6 +35,15 @@ elif os.path.exists("/usr/local/bin/cowsay"): # BSD path for cowsay cowsay = "/usr/local/bin/cowsay" + +# **************************************************************************** +# 1.1 DEV NOTES +# FIXME -- in order to make an ideal callback system, all of these should have +# access to the current task and/or play and host objects. We need to this +# while keeping present callbacks functionally intact and will do so. +# **************************************************************************** + + def call_callback_module(method_name, *args, **kwargs): for callback_plugin in utils.plugins.callback_loader.all(): @@ -209,6 +218,9 @@ class DefaultRunnerCallbacks(object): def on_async_failed(self, host, res, jid): call_callback_module('runner_on_async_failed', host, res, jid) + def on_file_diff(self, host, before_string, after_string): + call_callback_module('runner_on_file_diff', before_string, after_string) + ######################################################################## class CliRunnerCallbacks(DefaultRunnerCallbacks): @@ -272,6 +284,11 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks): print host_report_msg(host, self.options.module_name, result2, self.options.one_line) if self.options.tree: utils.write_tree_file(self.options.tree, host, utils.jsonify(result2,format=True)) + + def on_file_diff(self, host, before_string, after_string): + if self.options.diff: + print utils.get_diff(before_string, after_string) + super(CliRunnerCallbacks, self).on_file_diff(host, before_string, after_string) ######################################################################## @@ -404,6 +421,9 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks): print stringc(msg, 'red') super(PlaybookRunnerCallbacks, self).on_async_failed(host,res,jid) + def on_file_diff(self, host, before_string, after_string): + print utils.get_diff(before_string, after_string) + super(PlaybookRunnerCallbacks, self).on_file_diff(host, before_string, after_string) ######################################################################## diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 65057e1cff2..43974ccbffb 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -62,7 +62,8 @@ class PlayBook(object): only_tags = None, subset = C.DEFAULT_SUBSET, inventory = None, - check = False): + check = False, + diff = False): """ playbook: path to a playbook file @@ -94,6 +95,7 @@ class PlayBook(object): only_tags = [ 'all' ] self.check = check + self.diff = diff self.module_path = module_path self.forks = forks self.timeout = timeout @@ -271,7 +273,7 @@ class PlayBook(object): conditional=task.only_if, callbacks=self.runner_callbacks, sudo=task.sudo, sudo_user=task.sudo_user, transport=task.transport, sudo_pass=task.sudo_pass, is_playbook=True, - check=self.check + check=self.check, diff=self.diff ) if task.async_seconds == 0: @@ -377,7 +379,7 @@ class PlayBook(object): remote_pass=self.remote_pass, remote_port=play.remote_port, private_key_file=self.private_key_file, setup_cache=self.SETUP_CACHE, callbacks=self.runner_callbacks, sudo=play.sudo, sudo_user=play.sudo_user, transport=play.transport, sudo_pass=self.sudo_pass, is_playbook=True, module_vars=play.vars, - check=self.check + check=self.check, diff=self.diff ).run() self.stats.compute(setup_results, setup=True) diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index c67b12b6874..74d921fb841 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -118,11 +118,13 @@ class Runner(object): is_playbook=False, # running from playbook or not? inventory=None, # reference to Inventory object subset=None, # subset pattern - check=False # don't make any changes, just try to probe for potential changes + check=False, # don't make any changes, just try to probe for potential changes + diff=False ): # storage & defaults self.check = check + self.diff = diff self.setup_cache = utils.default(setup_cache, lambda: collections.defaultdict(dict)) self.basedir = utils.default(basedir, lambda: os.getcwd()) self.callbacks = utils.default(callbacks, lambda: DefaultRunnerCallbacks()) @@ -192,7 +194,7 @@ class Runner(object): # ***************************************************** def _execute_module(self, conn, tmp, module_name, args, - async_jid=None, async_module=None, async_limit=None, inject=None): + async_jid=None, async_module=None, async_limit=None, inject=None, persist_files=False): ''' runs a module that has already been transferred ''' @@ -233,7 +235,7 @@ class Runner(object): raise errors.AnsibleError("module is missing interpreter line") cmd = shebang.replace("#!","") + " " + cmd - if tmp.find("tmp") != -1 and C.DEFAULT_KEEP_REMOTE_FILES != '1': + if tmp.find("tmp") != -1 and C.DEFAULT_KEEP_REMOTE_FILES != '1' and not persist_files: cmd = cmd + "; rm -rf %s >/dev/null 2>&1" % tmp res = self._low_level_exec_command(conn, cmd, tmp, sudoable=True) data = utils.parse_json(res['stdout']) diff --git a/lib/ansible/runner/action_plugins/template.py b/lib/ansible/runner/action_plugins/template.py index 985892f5c06..4e533c2ced3 100644 --- a/lib/ansible/runner/action_plugins/template.py +++ b/lib/ansible/runner/action_plugins/template.py @@ -20,6 +20,7 @@ import os from ansible import utils from ansible import errors from ansible.runner.return_data import ReturnData +import base64 class ActionModule(object): @@ -46,6 +47,7 @@ class ActionModule(object): # if we have first_available_file in our vars # look up the files and use the first one we find as src + if 'first_available_file' in inject: found = False for fn in self.runner.module_vars.get('first_available_file'): @@ -79,18 +81,35 @@ class ActionModule(object): # template is different from the remote value - xfered = self.runner._transfer_str(conn, tmp, 'source', resultant) + # if showing diffs, we need to get the remote value + dest_contents = None + if self.runner.diff: + # using persist_files to keep the temp directory around to avoid needing to grab another + dest_result = self.runner._execute_module(conn, tmp, 'slurp', "path=%s" % dest, inject=inject, persist_files=True) + dest_contents = dest_result.result['content'] + if dest_result.result['encoding'] == 'base64': + dest_contents = base64.b64decode(dest_contents) + else: + raise Exception("unknown encoding, failed: %s" % dest_result.result) + + xfered = self.runner._transfer_str(conn, tmp, source, resultant) + # fix file permissions when the copy is done as a different user if self.runner.sudo and self.runner.sudo_user != 'root': self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered, tmp) # run the copy module module_args = "%s src=%s dest=%s" % (module_args, xfered, dest) - + if self.runner.check: + if self.runner.diff: + self.runner.callbacks.on_file_diff(conn.host, dest_contents, resultant) return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True)) else: - return self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject) + res = self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject) + if self.runner.diff: + self.runner.callbacks.on_file_diff(conn.host, dest_contents, resultant) + return res else: return ReturnData(conn=conn, comm_ok=True, result=dict(changed=False)) diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index f28eef0b548..dee9db645de 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -34,6 +34,7 @@ import termios import tty import pipes import random +import difflib VERBOSITY=0 @@ -395,7 +396,7 @@ def increment_debug(option, opt, value, parser): VERBOSITY += 1 def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, - async_opts=False, connect_opts=False, subset_opts=False, check_opts=False): + async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, diff_opts=False): ''' create an options parser for any ansible script ''' parser = SortedOptParser(usage, version=version("%prog")) @@ -457,6 +458,12 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, help="don't make any changes, instead try to predict some of the changes that may occur" ) + if diff_opts: + parser.add_option("-D", "--diff", default=False, dest='diff', action='store_true', + help="when changing (small) files and templates, show the differences in those files, works great with --check" + ) + + return parser def do_encrypt(result, encrypt, salt_size=None, salt=None): @@ -602,3 +609,10 @@ def make_sudo_cmd(sudo_user, executable, cmd): C.DEFAULT_SUDO_EXE, C.DEFAULT_SUDO_EXE, C.DEFAULT_SUDO_FLAGS, prompt, sudo_user, executable or '$SHELL', pipes.quote(cmd)) return ('/bin/sh -c ' + pipes.quote(sudocmd), prompt) + +def get_diff(before_string, after_string): + # called by --diff usage in playbook and runner via callbacks + # include names in diffs 'before' and 'after' and do diff -U 10 + differ = difflib.unified_diff(before_string.split("\n"), after_string.split("\n"), 'before', 'after', '', '', 10) + return "\n".join(list(differ)) + diff --git a/library/slurp b/library/slurp index ccc64950175..350ec871727 100644 --- a/library/slurp +++ b/library/slurp @@ -50,7 +50,7 @@ author: Michael DeHaan def main(): module = AnsibleModule( argument_spec = dict( - src = dict(required=True), + src = dict(required=True, aliases=['path']), ) ) source = module.params['src']