From f0743fc32a0a5dc0f38f8de490ff1004ac21b946 Mon Sep 17 00:00:00 2001 From: Stoned Elipot Date: Tue, 20 Aug 2013 23:09:44 +0200 Subject: [PATCH] Introduce the 'always_run' task clause. The 'always_run' task clause allows one to execute a task even in check mode. While here implement Runner.noop_on_check() to check if a runner really should execute its task, with respect to check mode option and 'always_run' clause. Also add the optional 'jinja2' argument to check_conditional() : it allows to give this function a jinja2 expression without exposing the 'jinja2_compare' implementation mechanism. --- docsite/latest/rst/playbooks2.rst | 23 +++++++++ lib/ansible/playbook/task.py | 9 ++-- lib/ansible/runner/__init__.py | 15 ++++++ lib/ansible/runner/action_plugins/add_host.py | 2 +- lib/ansible/runner/action_plugins/async.py | 2 +- lib/ansible/runner/action_plugins/copy.py | 4 +- lib/ansible/runner/action_plugins/fetch.py | 2 +- lib/ansible/runner/action_plugins/normal.py | 2 +- lib/ansible/runner/action_plugins/raw.py | 2 +- lib/ansible/runner/action_plugins/script.py | 2 +- lib/ansible/runner/action_plugins/template.py | 2 +- lib/ansible/utils/__init__.py | 5 +- test/TestPlayBook.py | 31 ++++++++++++ test/playbook-always-run.yml | 48 +++++++++++++++++++ 14 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 test/playbook-always-run.yml diff --git a/docsite/latest/rst/playbooks2.rst b/docsite/latest/rst/playbooks2.rst index 956668147bd..85a73eea4dd 100644 --- a/docsite/latest/rst/playbooks2.rst +++ b/docsite/latest/rst/playbooks2.rst @@ -1060,6 +1060,29 @@ Example:: ansible-playbook foo.yml --check +Running a task in check mode +```````````````````````````` + +.. versionadded:: 1.3 + +Sometimes you may want to have a task to be executed even in check +mode. To achieve this use the `always_run` clause on the task. Its +value is a Python expression, just like the `when` clause. In simple +cases a boolean YAML value would be sufficient as a value. + +Example:: + + tasks: + + - name: this task is run even in check mode + command: /something/to/run --even-in-check-mode + always_run: yes + +As a reminder, a task with a `when` clause evaluated to false, will +still be skipped even if it has a `always_run` clause evaluated to +true. + + Showing Differences with --diff ``````````````````````````````` diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 1566e77e907..a4f4911fba7 100644 --- a/lib/ansible/playbook/task.py +++ b/lib/ansible/playbook/task.py @@ -29,7 +29,7 @@ class Task(object): 'delegate_to', 'first_available_file', 'ignore_errors', 'local_action', 'transport', 'sudo', 'sudo_user', 'sudo_pass', 'items_lookup_plugin', 'items_lookup_terms', 'environment', 'args', - 'any_errors_fatal', 'changed_when' + 'any_errors_fatal', 'changed_when', 'always_run' ] # to prevent typos and such @@ -38,7 +38,7 @@ class Task(object): 'first_available_file', 'include', 'tags', 'register', 'ignore_errors', 'delegate_to', 'local_action', 'transport', 'sudo', 'sudo_user', 'sudo_pass', 'when', 'connection', 'environment', 'args', - 'any_errors_fatal', 'changed_when' + 'any_errors_fatal', 'changed_when', 'always_run' ] def __init__(self, play, ds, module_vars=None, additional_conditions=None): @@ -178,6 +178,8 @@ class Task(object): self.ignore_errors = ds.get('ignore_errors', False) self.any_errors_fatal = ds.get('any_errors_fatal', play.any_errors_fatal) + self.always_run = ds.get('always_run', False) + # action should be a string if not isinstance(self.action, basestring): raise errors.AnsibleError("action is of type '%s' and not a string in task. name: %s" % (type(self.action).__name__, self.name)) @@ -216,10 +218,11 @@ class Task(object): # allow runner to see delegate_to option self.module_vars['delegate_to'] = self.delegate_to - # make ignore_errors accessable to Runner code + # make some task attributes accessible to Runner code self.module_vars['ignore_errors'] = self.ignore_errors self.module_vars['register'] = self.register self.module_vars['changed_when'] = self.changed_when + self.module_vars['always_run'] = self.always_run # tags allow certain parts of a playbook to be run without running the whole playbook apply_tags = ds.get('tags', None) diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index 7f9e18aa1c5..d2591ba7ae9 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -37,6 +37,7 @@ import ansible.constants as C import ansible.inventory from ansible import utils from ansible.utils import template +from ansible.utils import check_conditional from ansible import errors from ansible import module_common import poller @@ -156,6 +157,7 @@ class Runner(object): self.inventory = utils.default(inventory, lambda: ansible.inventory.Inventory(host_list)) self.module_vars = utils.default(module_vars, lambda: {}) + self.always_run = None self.connector = connection.Connection(self) self.conditional = conditional self.module_name = module_name @@ -935,3 +937,16 @@ class Runner(object): self.background = time_limit results = self.run() return results, poller.AsyncPoller(results, self) + + # ***************************************************** + + def noop_on_check(self, inject): + ''' Should the runner run in check mode or not ? ''' + + # initialize self.always_run on first call + if self.always_run is None: + self.always_run = self.module_vars.get('always_run', False) + self.always_run = check_conditional( + self.always_run, self.basedir, inject, fail_on_undefined=True, jinja2=True) + + return (self.check and not self.always_run) diff --git a/lib/ansible/runner/action_plugins/add_host.py b/lib/ansible/runner/action_plugins/add_host.py index 1fdc75d3261..8c1219ef8e8 100644 --- a/lib/ansible/runner/action_plugins/add_host.py +++ b/lib/ansible/runner/action_plugins/add_host.py @@ -36,7 +36,7 @@ class ActionModule(object): def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): - if self.runner.check: + if self.runner.noop_on_check(inject): return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module')) args = {} diff --git a/lib/ansible/runner/action_plugins/async.py b/lib/ansible/runner/action_plugins/async.py index dcee36684d4..dee418e9886 100644 --- a/lib/ansible/runner/action_plugins/async.py +++ b/lib/ansible/runner/action_plugins/async.py @@ -25,7 +25,7 @@ class ActionModule(object): def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' transfer the given module name, plus the async module, then run it ''' - if self.runner.check: + if self.runner.noop_on_check(inject): 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 diff --git a/lib/ansible/runner/action_plugins/copy.py b/lib/ansible/runner/action_plugins/copy.py index 38d2ee6abe3..fd716451a76 100644 --- a/lib/ansible/runner/action_plugins/copy.py +++ b/lib/ansible/runner/action_plugins/copy.py @@ -126,7 +126,7 @@ class ActionModule(object): else: diff = {} - if self.runner.check: + if self.runner.noop_on_check(inject): if content is not None: os.remove(tmp_content) return ReturnData(conn=conn, result=dict(changed=True), diff=diff) @@ -172,7 +172,7 @@ class ActionModule(object): # don't send down raw=no module_args.pop('raw') module_args = "%s src=%s" % (module_args, pipes.quote(tmp_src)) - if self.runner.check: + if self.runner.noop_on_check(inject): module_args = "%s CHECKMODE=True" % module_args return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject, complex_args=complex_args) diff --git a/lib/ansible/runner/action_plugins/fetch.py b/lib/ansible/runner/action_plugins/fetch.py index e7afa7d955d..4f8de7e9dc2 100644 --- a/lib/ansible/runner/action_plugins/fetch.py +++ b/lib/ansible/runner/action_plugins/fetch.py @@ -36,7 +36,7 @@ class ActionModule(object): def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' handler for fetch operations ''' - if self.runner.check: + if self.runner.noop_on_check(inject): return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not (yet) supported for this module')) # load up options diff --git a/lib/ansible/runner/action_plugins/normal.py b/lib/ansible/runner/action_plugins/normal.py index a54090625a7..1df620c410a 100644 --- a/lib/ansible/runner/action_plugins/normal.py +++ b/lib/ansible/runner/action_plugins/normal.py @@ -38,7 +38,7 @@ class ActionModule(object): module_args = self.runner._complex_args_hack(complex_args, module_args) - if self.runner.check: + if self.runner.noop_on_check(inject): 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 diff --git a/lib/ansible/runner/action_plugins/raw.py b/lib/ansible/runner/action_plugins/raw.py index 94b73c50ea7..7fcfe92927a 100644 --- a/lib/ansible/runner/action_plugins/raw.py +++ b/lib/ansible/runner/action_plugins/raw.py @@ -30,7 +30,7 @@ class ActionModule(object): def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): - if self.runner.check: + if self.runner.noop_on_check(inject): # in --check mode, always skip this module execution return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True)) diff --git a/lib/ansible/runner/action_plugins/script.py b/lib/ansible/runner/action_plugins/script.py index 784ab58b163..17032220245 100644 --- a/lib/ansible/runner/action_plugins/script.py +++ b/lib/ansible/runner/action_plugins/script.py @@ -32,7 +32,7 @@ class ActionModule(object): def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs): ''' handler for file transfer operations ''' - if self.runner.check: + if self.runner.noop_on_check(inject): # in check mode, always skip this module return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module')) diff --git a/lib/ansible/runner/action_plugins/template.py b/lib/ansible/runner/action_plugins/template.py index a2eb51c1fae..9ddc3340a46 100644 --- a/lib/ansible/runner/action_plugins/template.py +++ b/lib/ansible/runner/action_plugins/template.py @@ -117,7 +117,7 @@ class ActionModule(object): # run the copy module module_args = "%s src=%s dest=%s original_basename=%s" % (module_args, pipes.quote(xfered), pipes.quote(dest), pipes.quote(os.path.basename(source))) - if self.runner.check: + if self.runner.noop_on_check(inject): return ReturnData(conn=conn, comm_ok=True, result=dict(changed=True), diff=dict(before_header=dest, after_header=source, before=dest_contents, after=resultant)) else: res = self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject, complex_args=complex_args) diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index 751052004e8..0a355b8a192 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -155,7 +155,10 @@ def is_changed(result): return (result.get('changed', False) in [ True, 'True', 'true']) -def check_conditional(conditional, basedir, inject, fail_on_undefined=False): +def check_conditional(conditional, basedir, inject, fail_on_undefined=False, jinja2=False): + + if jinja2: + conditional = "jinja2_compare %s" % conditional if conditional.startswith("jinja2_compare"): conditional = conditional.replace("jinja2_compare ","") diff --git a/test/TestPlayBook.py b/test/TestPlayBook.py index ac65ce5efc7..5f47649f2f8 100644 --- a/test/TestPlayBook.py +++ b/test/TestPlayBook.py @@ -474,6 +474,37 @@ class TestPlaybook(unittest.TestCase): assert utils.jsonify(expected, format=True) == utils.jsonify(actual,format=True) + + def test_playbook_always_run(self): + test_callbacks = TestCallbacks() + playbook = ansible.playbook.PlayBook( + playbook=os.path.join(self.test_dir, 'playbook-always-run.yml'), + host_list='test/ansible_hosts', + stats=ans_callbacks.AggregateStats(), + callbacks=test_callbacks, + runner_callbacks=test_callbacks, + check=True + ) + actual = playbook.run() + + # if different, this will output to screen + print "**ACTUAL**" + print utils.jsonify(actual, format=True) + expected = { + "localhost": { + "changed": 4, + "failures": 0, + "ok": 4, + "skipped": 8, + "unreachable": 0 + } + } + print "**EXPECTED**" + print utils.jsonify(expected, format=True) + + assert utils.jsonify(expected, format=True) == utils.jsonify(actual,format=True) + + def _compare_file_output(self, filename, expected_lines): actual_lines = [] with open(filename) as f: diff --git a/test/playbook-always-run.yml b/test/playbook-always-run.yml new file mode 100644 index 00000000000..9b6a921a671 --- /dev/null +++ b/test/playbook-always-run.yml @@ -0,0 +1,48 @@ +--- +- hosts: all + connection: local + gather_facts: False + vars: + var_true: True + var_false: False + var_empty_str: "''" + var_null: ~ + + tasks: + - action: command echo ping + always_run: yes + + - action: command echo pong 1 + + - action: command echo pong 2 + always_run: no + + - action: command echo pong 3 + always_run: 1 + 1 + + - action: command echo pong 4 + always_run: "''" + + - action: command echo pong 5 + always_run: False + + - action: command echo pong 6 + always_run: True + + - action: command echo pong 7 + always_run: var_true + + - action: command echo pong 8 + always_run: var_false + + - action: command echo pong 9 + always_run: var_empty_str + + - action: command echo pong 10 + always_run: var_null + + # this will never run... + - action: command echo pong 11 + always_run: yes + when: no +