Merge branch 'always_run-6' of https://github.com/stoned/ansible into stoned-always_run-6
This commit is contained in:
commit
157b697a83
14 changed files with 136 additions and 13 deletions
|
@ -1060,6 +1060,29 @@ Example::
|
||||||
|
|
||||||
ansible-playbook foo.yml --check
|
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
|
Showing Differences with --diff
|
||||||
```````````````````````````````
|
```````````````````````````````
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ class Task(object):
|
||||||
'delegate_to', 'first_available_file', 'ignore_errors',
|
'delegate_to', 'first_available_file', 'ignore_errors',
|
||||||
'local_action', 'transport', 'sudo', 'sudo_user', 'sudo_pass',
|
'local_action', 'transport', 'sudo', 'sudo_user', 'sudo_pass',
|
||||||
'items_lookup_plugin', 'items_lookup_terms', 'environment', 'args',
|
'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
|
# to prevent typos and such
|
||||||
|
@ -38,7 +38,7 @@ class Task(object):
|
||||||
'first_available_file', 'include', 'tags', 'register', 'ignore_errors',
|
'first_available_file', 'include', 'tags', 'register', 'ignore_errors',
|
||||||
'delegate_to', 'local_action', 'transport', 'sudo', 'sudo_user',
|
'delegate_to', 'local_action', 'transport', 'sudo', 'sudo_user',
|
||||||
'sudo_pass', 'when', 'connection', 'environment', 'args',
|
'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):
|
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.ignore_errors = ds.get('ignore_errors', False)
|
||||||
self.any_errors_fatal = ds.get('any_errors_fatal', play.any_errors_fatal)
|
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
|
# action should be a string
|
||||||
if not isinstance(self.action, basestring):
|
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))
|
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
|
# allow runner to see delegate_to option
|
||||||
self.module_vars['delegate_to'] = self.delegate_to
|
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['ignore_errors'] = self.ignore_errors
|
||||||
self.module_vars['register'] = self.register
|
self.module_vars['register'] = self.register
|
||||||
self.module_vars['changed_when'] = self.changed_when
|
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
|
# tags allow certain parts of a playbook to be run without running the whole playbook
|
||||||
apply_tags = ds.get('tags', None)
|
apply_tags = ds.get('tags', None)
|
||||||
|
|
|
@ -37,6 +37,7 @@ import ansible.constants as C
|
||||||
import ansible.inventory
|
import ansible.inventory
|
||||||
from ansible import utils
|
from ansible import utils
|
||||||
from ansible.utils import template
|
from ansible.utils import template
|
||||||
|
from ansible.utils import check_conditional
|
||||||
from ansible import errors
|
from ansible import errors
|
||||||
from ansible import module_common
|
from ansible import module_common
|
||||||
import poller
|
import poller
|
||||||
|
@ -156,6 +157,7 @@ class Runner(object):
|
||||||
self.inventory = utils.default(inventory, lambda: ansible.inventory.Inventory(host_list))
|
self.inventory = utils.default(inventory, lambda: ansible.inventory.Inventory(host_list))
|
||||||
|
|
||||||
self.module_vars = utils.default(module_vars, lambda: {})
|
self.module_vars = utils.default(module_vars, lambda: {})
|
||||||
|
self.always_run = None
|
||||||
self.connector = connection.Connection(self)
|
self.connector = connection.Connection(self)
|
||||||
self.conditional = conditional
|
self.conditional = conditional
|
||||||
self.module_name = module_name
|
self.module_name = module_name
|
||||||
|
@ -941,3 +943,16 @@ class Runner(object):
|
||||||
self.background = time_limit
|
self.background = time_limit
|
||||||
results = self.run()
|
results = self.run()
|
||||||
return results, poller.AsyncPoller(results, self)
|
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):
|
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'))
|
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not supported for this module'))
|
||||||
|
|
||||||
args = {}
|
args = {}
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ActionModule(object):
|
||||||
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
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 '''
|
''' 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'))
|
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
|
# shell and command module are the same
|
||||||
|
|
|
@ -126,7 +126,7 @@ class ActionModule(object):
|
||||||
else:
|
else:
|
||||||
diff = {}
|
diff = {}
|
||||||
|
|
||||||
if self.runner.check:
|
if self.runner.noop_on_check(inject):
|
||||||
if content is not None:
|
if content is not None:
|
||||||
os.remove(tmp_content)
|
os.remove(tmp_content)
|
||||||
return ReturnData(conn=conn, result=dict(changed=True), diff=diff)
|
return ReturnData(conn=conn, result=dict(changed=True), diff=diff)
|
||||||
|
@ -172,7 +172,7 @@ class ActionModule(object):
|
||||||
# don't send down raw=no
|
# don't send down raw=no
|
||||||
module_args.pop('raw')
|
module_args.pop('raw')
|
||||||
module_args = "%s src=%s" % (module_args, pipes.quote(tmp_src))
|
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
|
module_args = "%s CHECKMODE=True" % module_args
|
||||||
return self.runner._execute_module(conn, tmp, 'file', module_args, inject=inject, complex_args=complex_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):
|
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
||||||
''' handler for fetch operations '''
|
''' 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'))
|
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True, msg='check mode not (yet) supported for this module'))
|
||||||
|
|
||||||
# load up options
|
# load up options
|
||||||
|
|
|
@ -38,7 +38,7 @@ class ActionModule(object):
|
||||||
|
|
||||||
module_args = self.runner._complex_args_hack(complex_args, module_args)
|
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' ]:
|
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))
|
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
|
# 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):
|
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
|
# in --check mode, always skip this module execution
|
||||||
return ReturnData(conn=conn, comm_ok=True, result=dict(skipped=True))
|
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):
|
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
|
||||||
''' handler for file transfer operations '''
|
''' handler for file transfer operations '''
|
||||||
|
|
||||||
if self.runner.check:
|
if self.runner.noop_on_check(inject):
|
||||||
# in check mode, always skip this module
|
# 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'))
|
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
|
# 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)))
|
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))
|
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:
|
else:
|
||||||
res = self.runner._execute_module(conn, tmp, 'copy', module_args, inject=inject, complex_args=complex_args)
|
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'])
|
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"):
|
if conditional.startswith("jinja2_compare"):
|
||||||
conditional = conditional.replace("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)
|
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):
|
def _compare_file_output(self, filename, expected_lines):
|
||||||
actual_lines = []
|
actual_lines = []
|
||||||
with open(filename) as f:
|
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