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:
parent
3d6993221e
commit
a9162a86f2
8 changed files with 95 additions and 18 deletions
18
bin/ansible
18
bin/ansible
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
|
Loading…
Reference in a new issue