From fed82c2188828466202935e98e808a74903346d4 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sun, 3 Feb 2013 19:46:25 -0500 Subject: [PATCH] This implements a basic --check mode which for now is only implemented on template & copy operations. More detail will be shared with the list shortly. --- bin/ansible | 5 +-- bin/ansible-playbook | 5 ++- lib/ansible/module_common.py | 21 ++++++++++-- lib/ansible/playbook/__init__.py | 9 ++++-- lib/ansible/runner/__init__.py | 9 +++++- lib/ansible/runner/action_plugins/add_host.py | 4 +++ lib/ansible/runner/action_plugins/async.py | 5 +++ lib/ansible/runner/action_plugins/copy.py | 8 +++++ lib/ansible/runner/action_plugins/fail.py | 4 +++ lib/ansible/runner/action_plugins/fetch.py | 3 ++ lib/ansible/runner/action_plugins/group_by.py | 4 +++ lib/ansible/runner/action_plugins/normal.py | 7 ++++ lib/ansible/runner/action_plugins/pause.py | 6 +++- lib/ansible/runner/action_plugins/raw.py | 5 +++ lib/ansible/runner/action_plugins/script.py | 4 +++ lib/ansible/runner/action_plugins/template.py | 32 +++++++++++++------ lib/ansible/utils/__init__.py | 7 +++- library/copy | 2 +- library/file | 3 +- library/ping | 3 +- library/setup | 3 +- 21 files changed, 125 insertions(+), 24 deletions(-) diff --git a/bin/ansible b/bin/ansible index 394d8a7942..102390f42a 100755 --- a/bin/ansible +++ b/bin/ansible @@ -46,7 +46,7 @@ class Cli(object): ''' 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, usage='%prog [options]') + output_opts=True, connect_opts=True, check_opts=True, 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', @@ -109,7 +109,8 @@ class Cli(object): pattern=pattern, callbacks=self.callbacks, sudo=options.sudo, sudo_pass=sudopass,sudo_user=options.sudo_user, - transport=options.connection, subset=options.subset + transport=options.connection, subset=options.subset, + check=options.check ) if options.seconds: diff --git a/bin/ansible-playbook b/bin/ansible-playbook index 060bbe284e..138e5a0d93 100755 --- a/bin/ansible-playbook +++ b/bin/ansible-playbook @@ -52,11 +52,13 @@ 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) + parser = utils.base_parser(constants=C, usage=usage, connect_opts=True, + runas_opts=True, subset_opts=True, check_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', help="only run plays and tasks tagged with these values") + # FIXME: list hosts is a common option and can be moved to utils/__init__.py parser.add_option('--list-hosts', dest='listhosts', action='store_true', help="dump out a list of hosts, each play will run against, does not run playbook!") parser.add_option('--syntax-check', dest='syntax', action='store_true', @@ -120,6 +122,7 @@ def main(args): extra_vars=extra_vars, private_key_file=options.private_key_file, only_tags=only_tags, + check=options.check ) if options.listhosts: diff --git a/lib/ansible/module_common.py b/lib/ansible/module_common.py index 38d5666cba..2a10603117 100644 --- a/lib/ansible/module_common.py +++ b/lib/ansible/module_common.py @@ -133,7 +133,7 @@ class AnsibleModule(object): def __init__(self, argument_spec, bypass_checks=False, no_log=False, check_invalid_arguments=True, mutually_exclusive=None, required_together=None, - required_one_of=None, add_file_common_args=False): + required_one_of=None, add_file_common_args=False, supports_check_mode=False): ''' common code for quickly building an ansible module in Python @@ -142,6 +142,8 @@ class AnsibleModule(object): ''' self.argument_spec = argument_spec + self.supports_check_mode = supports_check_mode + self.check_mode = False if add_file_common_args: self.argument_spec.update(FILE_COMMON_ARGUMENTS) @@ -149,7 +151,7 @@ class AnsibleModule(object): os.environ['LANG'] = MODULE_LANG (self.params, self.args) = self._load_params() - self._legal_inputs = [] + self._legal_inputs = [ 'CHECKMODE' ] self._handle_aliases() if check_invalid_arguments: @@ -300,7 +302,9 @@ class AnsibleModule(object): if context[i] is None: new_context[i] = cur_context[i] if cur_context != new_context: - try: + try: + if self.check_mode: + return True rc = selinux.lsetfilecon(path, ':'.join(new_context)) except OSError: self.fail_json(path=path, msg='invalid selinux context', new_context=new_context, cur_context=cur_context, input_was=context) @@ -319,6 +323,8 @@ class AnsibleModule(object): uid = pwd.getpwnam(owner).pw_uid except KeyError: self.fail_json(path=path, msg='chown failed: failed to look up user %s' % owner) + if self.check_mode: + return True try: os.chown(path, uid, -1) except OSError: @@ -332,6 +338,8 @@ class AnsibleModule(object): return changed old_user, old_group = self.user_and_group(path) if old_group != group: + if self.check_mode: + return True try: gid = grp.getgrnam(group).gr_gid except KeyError: @@ -357,6 +365,8 @@ class AnsibleModule(object): prev_mode = stat.S_IMODE(st[stat.ST_MODE]) if prev_mode != mode: + if self.check_mode: + return True # FIXME: comparison against string above will cause this to be executed # every time try: @@ -451,6 +461,11 @@ class AnsibleModule(object): def _check_invalid_arguments(self): for (k,v) in self.params.iteritems(): + if k == 'CHECKMODE': + if not self.supports_check_mode: + self.exit_json(skipped=True, msg="remote module does not support check mode") + if self.supports_check_mode: + self.check_mode = True if k not in self._legal_inputs: self.fail_json(msg="unsupported parameter for module: %s" % k) diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index 1131f7589a..65057e1cff 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -61,7 +61,8 @@ class PlayBook(object): extra_vars = None, only_tags = None, subset = C.DEFAULT_SUBSET, - inventory = None): + inventory = None, + check = False): """ playbook: path to a playbook file @@ -79,6 +80,7 @@ class PlayBook(object): stats: holds aggregrate data about events occuring to each host sudo: if not specified per play, requests all plays use sudo mode inventory: can be specified instead of host_list to use a pre-existing inventory object + check: don't change anything, just try to detect some potential changes """ self.SETUP_CACHE = SETUP_CACHE @@ -91,6 +93,7 @@ class PlayBook(object): if only_tags is None: only_tags = [ 'all' ] + self.check = check self.module_path = module_path self.forks = forks self.timeout = timeout @@ -267,7 +270,8 @@ class PlayBook(object): setup_cache=self.SETUP_CACHE, basedir=task.play.basedir, 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 + transport=task.transport, sudo_pass=task.sudo_pass, is_playbook=True, + check=self.check ) if task.async_seconds == 0: @@ -373,6 +377,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 ).run() self.stats.compute(setup_results, setup=True) diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index bd35c7557d..5d913f50cd 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -114,10 +114,12 @@ class Runner(object): module_vars=None, # a playbooks internals thing is_playbook=False, # running from playbook or not? 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 ): # storage & defaults + self.check = check 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()) @@ -207,6 +209,11 @@ class Runner(object): cmd = "" if not is_new_style: + if 'CHECKMODE=True' in args: + # if module isn't using AnsibleModuleCommon infrastructure we can't be certain it knows how to + # do --check mode, so to be safe we will not run it. + return ReturnData(conn=conn, result=dict(skippped=True, msg="cannot run check mode against old-style modules")) + args = utils.template(self.basedir, args, inject) argsfile = self._transfer_str(conn, tmp, 'arguments', args) if async_jid is None: diff --git a/lib/ansible/runner/action_plugins/add_host.py b/lib/ansible/runner/action_plugins/add_host.py index f2ce7ccfe9..a97cb11402 100644 --- a/lib/ansible/runner/action_plugins/add_host.py +++ b/lib/ansible/runner/action_plugins/add_host.py @@ -35,6 +35,10 @@ class ActionModule(object): self.runner = runner def run(self, conn, tmp, module_name, module_args, inject): + + if self.runner.check: + return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module') + args = parse_kv(module_args) if not 'hostname' in args: raise ae("'hostname' is a required argument.") diff --git a/lib/ansible/runner/action_plugins/async.py b/lib/ansible/runner/action_plugins/async.py index 73b1dd3f7b..ab56404f65 100644 --- a/lib/ansible/runner/action_plugins/async.py +++ b/lib/ansible/runner/action_plugins/async.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +from ansible.runner.return_data import ReturnData + class ActionModule(object): def __init__(self, runner): @@ -23,6 +25,9 @@ class ActionModule(object): def run(self, conn, tmp, module_name, module_args, inject): ''' transfer the given module name, plus the async module, then run it ''' + if self.runner.check: + return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module')) + # shell and command module are the same if module_name == 'shell': module_name = 'command' diff --git a/lib/ansible/runner/action_plugins/copy.py b/lib/ansible/runner/action_plugins/copy.py index 3eb9c4ed91..f3625724fc 100644 --- a/lib/ansible/runner/action_plugins/copy.py +++ b/lib/ansible/runner/action_plugins/copy.py @@ -69,6 +69,12 @@ class ActionModule(object): exec_rc = None if local_md5 != remote_md5: + + if self.runner.check: + # TODO: if the filesize is small, include a nice pretty-printed diff by + # calling a (new) diff callback + return ReturnData(conn=conn, result=dict(changed=True)) + # transfer the file to a remote tmp location tmp_src = tmp + os.path.basename(source) conn.put_file(source, tmp_src) @@ -86,5 +92,7 @@ class ActionModule(object): tmp_src = tmp + os.path.basename(source) module_args = "%s src=%s" % (module_args, tmp_src) + if self.runner.check: + module_args = "%s CHECKMODE=True" % module_args return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject) diff --git a/lib/ansible/runner/action_plugins/fail.py b/lib/ansible/runner/action_plugins/fail.py index f835a1f493..df06b3225e 100644 --- a/lib/ansible/runner/action_plugins/fail.py +++ b/lib/ansible/runner/action_plugins/fail.py @@ -29,6 +29,10 @@ class ActionModule(object): self.runner = runner def run(self, conn, tmp, module_name, module_args, inject): + + # note: the fail module does not need to pay attention to check mode + # it always runs. + args = utils.parse_kv(module_args) if not 'msg' in args: args['msg'] = 'Failed as requested from task' diff --git a/lib/ansible/runner/action_plugins/fetch.py b/lib/ansible/runner/action_plugins/fetch.py index 043d64a935..2c0a3b0a4b 100644 --- a/lib/ansible/runner/action_plugins/fetch.py +++ b/lib/ansible/runner/action_plugins/fetch.py @@ -36,6 +36,9 @@ class ActionModule(object): def run(self, conn, tmp, module_name, module_args, inject): ''' handler for fetch operations ''' + if self.runner.check: + return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not (yet) supported for this module')) + # load up options options = utils.parse_kv(module_args) source = options.get('src', None) diff --git a/lib/ansible/runner/action_plugins/group_by.py b/lib/ansible/runner/action_plugins/group_by.py index 7e8a6d563b..aee6b64730 100644 --- a/lib/ansible/runner/action_plugins/group_by.py +++ b/lib/ansible/runner/action_plugins/group_by.py @@ -33,6 +33,10 @@ class ActionModule(object): self.runner = runner def run(self, conn, tmp, module_name, module_args, inject): + + # the group_by module does not need to pay attention to check mode. + # it always runs. + args = parse_kv(self.runner.module_args) if not 'key' in args: raise ae("'key' is a required argument.") diff --git a/lib/ansible/runner/action_plugins/normal.py b/lib/ansible/runner/action_plugins/normal.py index a93553b20b..e077e29513 100644 --- a/lib/ansible/runner/action_plugins/normal.py +++ b/lib/ansible/runner/action_plugins/normal.py @@ -36,6 +36,13 @@ class ActionModule(object): def run(self, conn, tmp, module_name, module_args, inject): ''' transfer & execute a module that is not 'copy' or 'template' ''' + if self.runner.check: + if module_name in [ 'shell', 'command' ]: + return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for %s' % module_name)) + # else let the module parsing code decide, though this will only be allowed for AnsibleModuleCommon using + # python modules for now + module_args += " CHECKMODE=True" + # shell and command are the same module if module_name == 'shell': module_name = 'command' diff --git a/lib/ansible/runner/action_plugins/pause.py b/lib/ansible/runner/action_plugins/pause.py index 6c9f732bd6..d6ea53f4a3 100644 --- a/lib/ansible/runner/action_plugins/pause.py +++ b/lib/ansible/runner/action_plugins/pause.py @@ -47,7 +47,11 @@ class ActionModule(object): } def run(self, conn, tmp, module_name, module_args, inject): - ''' run the pause actionmodule ''' + ''' run the pause action module ''' + + # note: this module does not need to pay attention to the 'check' + # flag, it always runs + hosts = ', '.join(self.runner.host_set) args = parse_kv(template(self.runner.basedir, module_args, inject)) diff --git a/lib/ansible/runner/action_plugins/raw.py b/lib/ansible/runner/action_plugins/raw.py index f083694e68..c05a2344ed 100644 --- a/lib/ansible/runner/action_plugins/raw.py +++ b/lib/ansible/runner/action_plugins/raw.py @@ -29,6 +29,11 @@ class ActionModule(object): self.runner = runner def run(self, conn, tmp, module_name, module_args, inject): + + if self.runner.check: + # in --check mode, always skip this module execution + return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True)) + executable = '' # From library/command, keep in sync r = re.compile(r'(^|\s)(executable)=(?P[\'"])?(.*?)(?(quote)(?