Very basic --diff option for showing what happens when templates change.

Probably output is not useful if not used with --limit

Works well with --check mode
This commit is contained in:
Michael DeHaan 2013-02-07 22:51:33 -05:00
parent 3d6993221e
commit a9162a86f2
8 changed files with 95 additions and 18 deletions

View file

@ -45,8 +45,18 @@ class Cli(object):
def parse(self): def parse(self):
''' create an options parser for bin/ansible ''' ''' create an options parser for bin/ansible '''
parser = utils.base_parser(constants=C, runas_opts=True, subset_opts=True, async_opts=True, parser = utils.base_parser(
output_opts=True, connect_opts=True, check_opts=True, usage='%prog <host-pattern> [options]') 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 <host-pattern> [options]'
)
parser.add_option('-a', '--args', dest='module_args', parser.add_option('-a', '--args', dest='module_args',
help="module arguments", default=C.DEFAULT_MODULE_ARGS) help="module arguments", default=C.DEFAULT_MODULE_ARGS)
parser.add_option('-m', '--module-name', dest='module_name', parser.add_option('-m', '--module-name', dest='module_name',
@ -54,6 +64,7 @@ class Cli(object):
default=C.DEFAULT_MODULE_NAME) default=C.DEFAULT_MODULE_NAME)
parser.add_option('--list-hosts', dest='listhosts', action='store_true', 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!") help="dump out a list of hosts matching input pattern, does not execute any modules!")
options, args = parser.parse_args() options, args = parser.parse_args()
self.callbacks.options = options self.callbacks.options = options
@ -112,7 +123,8 @@ class Cli(object):
callbacks=self.callbacks, sudo=options.sudo, callbacks=self.callbacks, sudo=options.sudo,
sudo_pass=sudopass,sudo_user=options.sudo_user, sudo_pass=sudopass,sudo_user=options.sudo_user,
transport=options.connection, subset=options.subset, transport=options.connection, subset=options.subset,
check=options.check check=options.check,
diff=options.check
) )
if options.seconds: if options.seconds:

View file

@ -52,8 +52,15 @@ def main(args):
# create parser for CLI options # create parser for CLI options
usage = "%prog playbook.yml" usage = "%prog playbook.yml"
parser = utils.base_parser(constants=C, usage=usage, connect_opts=True, parser = utils.base_parser(
runas_opts=True, subset_opts=True, check_opts=True) 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, parser.add_option('-e', '--extra-vars', dest="extra_vars", default=None,
help="set additional key=value variables from the CLI") help="set additional key=value variables from the CLI")
parser.add_option('-t', '--tags', dest='tags', default='all', parser.add_option('-t', '--tags', dest='tags', default='all',
@ -122,7 +129,8 @@ def main(args):
extra_vars=extra_vars, extra_vars=extra_vars,
private_key_file=options.private_key_file, private_key_file=options.private_key_file,
only_tags=only_tags, only_tags=only_tags,
check=options.check check=options.check,
diff=options.diff
) )
if options.listhosts: if options.listhosts:

View file

@ -1,4 +1,4 @@
# (C) 2012, Michael DeHaan, <michael.dehaan@gmail.com> # (C) 2012-2013, Michael DeHaan, <michael.dehaan@gmail.com>
# This file is part of Ansible # This file is part of Ansible
# #
@ -35,6 +35,15 @@ elif os.path.exists("/usr/local/bin/cowsay"):
# BSD path for cowsay # BSD path for cowsay
cowsay = "/usr/local/bin/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): def call_callback_module(method_name, *args, **kwargs):
for callback_plugin in utils.plugins.callback_loader.all(): for callback_plugin in utils.plugins.callback_loader.all():
@ -209,6 +218,9 @@ class DefaultRunnerCallbacks(object):
def on_async_failed(self, host, res, jid): def on_async_failed(self, host, res, jid):
call_callback_module('runner_on_async_failed', 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): class CliRunnerCallbacks(DefaultRunnerCallbacks):
@ -273,6 +285,11 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
if self.options.tree: if self.options.tree:
utils.write_tree_file(self.options.tree, host, utils.jsonify(result2,format=True)) 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)
######################################################################## ########################################################################
class PlaybookRunnerCallbacks(DefaultRunnerCallbacks): class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
@ -404,6 +421,9 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
print stringc(msg, 'red') print stringc(msg, 'red')
super(PlaybookRunnerCallbacks, self).on_async_failed(host,res,jid) 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)
######################################################################## ########################################################################

View file

@ -62,7 +62,8 @@ class PlayBook(object):
only_tags = None, only_tags = None,
subset = C.DEFAULT_SUBSET, subset = C.DEFAULT_SUBSET,
inventory = None, inventory = None,
check = False): check = False,
diff = False):
""" """
playbook: path to a playbook file playbook: path to a playbook file
@ -94,6 +95,7 @@ class PlayBook(object):
only_tags = [ 'all' ] only_tags = [ 'all' ]
self.check = check self.check = check
self.diff = diff
self.module_path = module_path self.module_path = module_path
self.forks = forks self.forks = forks
self.timeout = timeout self.timeout = timeout
@ -271,7 +273,7 @@ class PlayBook(object):
conditional=task.only_if, callbacks=self.runner_callbacks, conditional=task.only_if, callbacks=self.runner_callbacks,
sudo=task.sudo, sudo_user=task.sudo_user, sudo=task.sudo, sudo_user=task.sudo_user,
transport=task.transport, sudo_pass=task.sudo_pass, is_playbook=True, 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: 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, 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, 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, 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() ).run()
self.stats.compute(setup_results, setup=True) self.stats.compute(setup_results, setup=True)

View file

@ -118,11 +118,13 @@ class Runner(object):
is_playbook=False, # running from playbook or not? is_playbook=False, # running from playbook or not?
inventory=None, # reference to Inventory object inventory=None, # reference to Inventory object
subset=None, # subset pattern 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 # storage & defaults
self.check = check self.check = check
self.diff = diff
self.setup_cache = utils.default(setup_cache, lambda: collections.defaultdict(dict)) self.setup_cache = utils.default(setup_cache, lambda: collections.defaultdict(dict))
self.basedir = utils.default(basedir, lambda: os.getcwd()) self.basedir = utils.default(basedir, lambda: os.getcwd())
self.callbacks = utils.default(callbacks, lambda: DefaultRunnerCallbacks()) self.callbacks = utils.default(callbacks, lambda: DefaultRunnerCallbacks())
@ -192,7 +194,7 @@ class Runner(object):
# ***************************************************** # *****************************************************
def _execute_module(self, conn, tmp, module_name, args, 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 ''' ''' runs a module that has already been transferred '''
@ -233,7 +235,7 @@ class Runner(object):
raise errors.AnsibleError("module is missing interpreter line") raise errors.AnsibleError("module is missing interpreter line")
cmd = shebang.replace("#!","") + " " + cmd 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 cmd = cmd + "; rm -rf %s >/dev/null 2>&1" % tmp
res = self._low_level_exec_command(conn, cmd, tmp, sudoable=True) res = self._low_level_exec_command(conn, cmd, tmp, sudoable=True)
data = utils.parse_json(res['stdout']) data = utils.parse_json(res['stdout'])

View file

@ -20,6 +20,7 @@ import os
from ansible import utils from ansible import utils
from ansible import errors from ansible import errors
from ansible.runner.return_data import ReturnData from ansible.runner.return_data import ReturnData
import base64
class ActionModule(object): class ActionModule(object):
@ -46,6 +47,7 @@ class ActionModule(object):
# if we have first_available_file in our vars # if we have first_available_file in our vars
# look up the files and use the first one we find as src # look up the files and use the first one we find as src
if 'first_available_file' in inject: if 'first_available_file' in inject:
found = False found = False
for fn in self.runner.module_vars.get('first_available_file'): for fn in self.runner.module_vars.get('first_available_file'):
@ -79,7 +81,19 @@ class ActionModule(object):
# template is different from the remote value # 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 # fix file permissions when the copy is done as a different user
if self.runner.sudo and self.runner.sudo_user != 'root': if self.runner.sudo and self.runner.sudo_user != 'root':
self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered, tmp) self.runner._low_level_exec_command(conn, "chmod a+r %s" % xfered, tmp)
@ -88,9 +102,14 @@ class ActionModule(object):
module_args = "%s src=%s dest=%s" % (module_args, xfered, dest) module_args = "%s src=%s dest=%s" % (module_args, xfered, dest)
if self.runner.check: 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)) return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True))
else: 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: else:
return ReturnData(conn=conn, comm_ok=True, result=dict(changed=False)) return ReturnData(conn=conn, comm_ok=True, result=dict(changed=False))

View file

@ -34,6 +34,7 @@ import termios
import tty import tty
import pipes import pipes
import random import random
import difflib
VERBOSITY=0 VERBOSITY=0
@ -395,7 +396,7 @@ def increment_debug(option, opt, value, parser):
VERBOSITY += 1 VERBOSITY += 1
def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, 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 ''' ''' create an options parser for any ansible script '''
parser = SortedOptParser(usage, version=version("%prog")) 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" 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 return parser
def do_encrypt(result, encrypt, salt_size=None, salt=None): 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, C.DEFAULT_SUDO_EXE, C.DEFAULT_SUDO_EXE, C.DEFAULT_SUDO_FLAGS,
prompt, sudo_user, executable or '$SHELL', pipes.quote(cmd)) prompt, sudo_user, executable or '$SHELL', pipes.quote(cmd))
return ('/bin/sh -c ' + pipes.quote(sudocmd), prompt) 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))

View file

@ -50,7 +50,7 @@ author: Michael DeHaan
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict( argument_spec = dict(
src = dict(required=True), src = dict(required=True, aliases=['path']),
) )
) )
source = module.params['src'] source = module.params['src']