mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
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.
This commit is contained in:
parent
7ac3bbc198
commit
f0743fc32a
14 changed files with 136 additions and 13 deletions
|
@ -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:
|
||||
|
|
48
test/playbook-always-run.yml
Normal file
48
test/playbook-always-run.yml
Normal file
|
@ -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…
Reference in a new issue