From eb45f07ae3a64a8ff675bd0846d156522c1e14fc Mon Sep 17 00:00:00 2001 From: Stoned Elipot Date: Sun, 14 Jul 2013 21:07:45 +0200 Subject: [PATCH 1/3] Introduce 'changed_when' keyword to override a task's changed status with the evaluation of a Jinja2 expression --- lib/ansible/playbook/__init__.py | 4 ++-- lib/ansible/playbook/task.py | 14 ++++++++++---- lib/ansible/runner/__init__.py | 10 ++++++++++ test/TestPlayBook.py | 28 ++++++++++++++++++++++++++++ test/playbook-changed_when.yml | 25 +++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 test/playbook-changed_when.yml diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py index f554bd3e319..cfaaa7cb4ec 100644 --- a/lib/ansible/playbook/__init__.py +++ b/lib/ansible/playbook/__init__.py @@ -351,7 +351,7 @@ class PlayBook(object): # extra vars need to always trump - so update again following the facts self.SETUP_CACHE[host].update(self.extra_vars) if task.register: - if 'stdout' in result: + if 'stdout' in result and 'stdout_lines' not in result: result['stdout_lines'] = result['stdout'].splitlines() self.SETUP_CACHE[host][task.register] = result @@ -359,7 +359,7 @@ class PlayBook(object): if task.ignore_errors and task.register: failed = results.get('failed', {}) for host, result in failed.iteritems(): - if 'stdout' in result: + if 'stdout' in result and 'stdout_lines' not in result: result['stdout_lines'] = result['stdout'].splitlines() self.SETUP_CACHE[host][task.register] = result diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py index 7a34601c7e6..1566e77e907 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' + 'any_errors_fatal', 'changed_when' ] # 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' + 'any_errors_fatal', 'changed_when' ] def __init__(self, play, ds, module_vars=None, additional_conditions=None): @@ -88,8 +88,8 @@ class Task(object): else: raise errors.AnsibleError("cannot find lookup plugin named %s for usage in with_%s" % (plugin_name, plugin_name)) - elif x == 'when': - ds['when'] = "jinja2_compare %s" % (ds[x]) + elif x in [ 'changed_when', 'when']: + ds[x] = "jinja2_compare %s" % (ds[x]) elif x.startswith("when_"): if 'when' in ds: raise errors.AnsibleError("multiple when_* statements specified in task %s" % (ds.get('name', ds['action']))) @@ -161,6 +161,10 @@ class Task(object): # load various attributes self.only_if = ds.get('only_if', 'True') self.when = ds.get('when', None) + self.changed_when = ds.get('changed_when', None) + + if self.changed_when is not None: + self.changed_when = utils.compile_when_to_only_if(self.changed_when) self.async_seconds = int(ds.get('async', 0)) # not async by default self.async_poll_interval = int(ds.get('poll', 10)) # default poll = 10 seconds @@ -214,6 +218,8 @@ class Task(object): # make ignore_errors accessable 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 # 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 914e87e21c8..a2bc7373894 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -630,6 +630,16 @@ class Runner(object): module_name=module_name ) + changed_when = self.module_vars.get('changed_when') + if changed_when is not None: + register = self.module_vars.get('register') + if register is not None: + if 'stdout' in data: + data['stdout_lines'] = data['stdout'].splitlines() + inject[register] = data + changed = template.template(self.basedir, changed_when, inject, fail_on_undefined=self.error_on_undefined_vars) + data['changed'] = utils.check_conditional(changed) + if is_chained: # no callbacks return result diff --git a/test/TestPlayBook.py b/test/TestPlayBook.py index 5b44302af09..6f005520760 100644 --- a/test/TestPlayBook.py +++ b/test/TestPlayBook.py @@ -382,6 +382,34 @@ class TestPlaybook(unittest.TestCase): assert utils.jsonify(expected, format=True) == utils.jsonify(actual,format=True) + def test_playbook_changed_when(self): + test_callbacks = TestCallbacks() + playbook = ansible.playbook.PlayBook( + playbook=os.path.join(self.test_dir, 'playbook-changed_when.yml'), + host_list='test/ansible_hosts', + stats=ans_callbacks.AggregateStats(), + callbacks=test_callbacks, + runner_callbacks=test_callbacks + ) + actual = playbook.run() + + # if different, this will output to screen + print "**ACTUAL**" + print utils.jsonify(actual, format=True) + expected = { + "localhost": { + "changed": 3, + "failures": 0, + "ok": 6, + "skipped": 0, + "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-changed_when.yml b/test/playbook-changed_when.yml new file mode 100644 index 00000000000..9625d5d6551 --- /dev/null +++ b/test/playbook-changed_when.yml @@ -0,0 +1,25 @@ +--- +- hosts: all + connection: local + gather_facts: False + + tasks: + - action: command echo first action + - action: command echo second action + register: var + changed_when: "'X' in var.stdout" + - action: shell exit 2 + register: exit + ignore_errors: yes + changed_when: "exit.rc < 1" + - action: command echo third action + changed_when: false + - action: file path=/ state=directory + changed_when: true + - action: command echo {{item}} + register: out + changed_when: "'e' in out.stdout" + with_items: + - hello + - foo + - bye From 7f462a346a38ad45f50f3754e71b8573decf7290 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sun, 21 Jul 2013 10:34:47 -0400 Subject: [PATCH 2/3] Work in progress on merging changed_when. --- lib/ansible/runner/__init__.py | 6 +++--- lib/ansible/runner/action_plugins/group_by.py | 2 +- lib/ansible/utils/__init__.py | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/ansible/runner/__init__.py b/lib/ansible/runner/__init__.py index e5d62e3a49e..72423574579 100644 --- a/lib/ansible/runner/__init__.py +++ b/lib/ansible/runner/__init__.py @@ -528,7 +528,8 @@ class Runner(object): self.conditional = [ self.conditional ] for cond in self.conditional: - if not utils.check_conditional(cond, self.basedir, inject): + + if not utils.check_conditional(cond, self.basedir, inject, fail_on_undefined=self.error_on_undefined_vars): result = utils.jsonify(dict(changed=False, skipped=True)) self.callbacks.on_skipped(host, inject.get('item',None)) return ReturnData(host=host, result=result) @@ -636,8 +637,7 @@ class Runner(object): if 'stdout' in data: data['stdout_lines'] = data['stdout'].splitlines() inject[register] = data - changed = template.template(self.basedir, changed_when, inject, fail_on_undefined=self.error_on_undefined_vars) - data['changed'] = utils.check_conditional(changed) + data['changed'] = utils.check_conditional(changed_when, self.basedir, inject, fail_on_undefined=self.error_on_undefined_vars) if is_chained: # no callbacks diff --git a/lib/ansible/runner/action_plugins/group_by.py b/lib/ansible/runner/action_plugins/group_by.py index 093196e399b..79bcea42c19 100644 --- a/lib/ansible/runner/action_plugins/group_by.py +++ b/lib/ansible/runner/action_plugins/group_by.py @@ -58,7 +58,7 @@ class ActionModule(object): data = {} data.update(inject) data.update(inject['hostvars'][host]) - if not check_conditional(self.runner.basedir, self.runner.conditional, data): + if not check_conditional(self.runner.basedir, self.runner.conditional, data, fail_on_undefined=self.runner.error_on_undefined_vars): continue group_name = template.template(self.runner.basedir, args['key'], data) group_name = group_name.replace(' ','-') diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index 572e101cb18..70f16ae3370 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -155,16 +155,18 @@ def is_changed(result): return (result.get('changed', False) in [ True, 'True', 'true']) -def check_conditional(conditional, basedir, inject): +def check_conditional(conditional, basedir, inject, fail_on_undefined=False): if conditional.startswith("jinja2_compare"): conditional = conditional.replace("jinja2_compare ","") # allow variable names if conditional in inject: conditional = inject[conditional] - conditional = template.template(basedir, conditional, inject) + print "INJECTIFYING: %s" % inject + conditional = template.template(basedir, conditional, inject, fail_on_undefined=fail_on_undefined) # a Jinja2 evaluation that results in something Python can eval! presented = "{% if " + conditional + " %} True {% else %} False {% endif %}" + return presented if not isinstance(conditional, basestring): return conditional From 0756aa406a24539e565c748dc9eebbd1c60abb46 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Sun, 21 Jul 2013 10:37:02 -0400 Subject: [PATCH 3/3] Change conditional operation workflow. Conflicts: lib/ansible/utils/__init__.py --- lib/ansible/utils/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index 70f16ae3370..a92c82caebe 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -162,11 +162,17 @@ def check_conditional(conditional, basedir, inject, fail_on_undefined=False): # allow variable names if conditional in inject: conditional = inject[conditional] - print "INJECTIFYING: %s" % inject conditional = template.template(basedir, conditional, inject, fail_on_undefined=fail_on_undefined) # a Jinja2 evaluation that results in something Python can eval! presented = "{% if " + conditional + " %} True {% else %} False {% endif %}" - return presented + conditional = template.template(basedir, presented, inject) + val = conditional.lstrip().rstrip() + if val == "True": + return True + elif val == "False": + return False + else: + raise errors.AnsibleError("unable to evaluate conditional: %s" % conditional) if not isinstance(conditional, basestring): return conditional