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.
pull/3899/head
Stoned Elipot 11 years ago
parent 7ac3bbc198
commit f0743fc32a

@ -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
```````````````````````````````

@ -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)

@ -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)

@ -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 = {}

@ -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

@ -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)

@ -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

@ -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

@ -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))

@ -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'))

@ -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)

@ -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 ","")

@ -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:

@ -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
Loading…
Cancel
Save