New keyword: ignore_unreachable (#43857)
This commit is contained in:
parent
2603604fd6
commit
653d9c0f87
15 changed files with 129 additions and 2 deletions
2
changelogs/fragments/ignore_unreachable.yml
Normal file
2
changelogs/fragments/ignore_unreachable.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
major_changes:
|
||||
- New keyword `ignore_unreachable` for plays and blocks. Allows ignoring tasks that fail due to unreachable hosts, and check results with `is unreachable` test.
|
|
@ -37,6 +37,7 @@ gather_timeout: Allows you to set the timeout for the fact gathering plugin cont
|
|||
handlers: "A section with tasks that are treated as handlers, these won't get executed normally, only when notified after each section of tasks is complete."
|
||||
hosts: "A list of groups, hosts or host pattern that translates into a list of hosts that are the play's target."
|
||||
ignore_errors: Boolean that allows you to ignore task failures and continue with play. It does not affect connection errors.
|
||||
ignore_unreachable: Boolean that allows you to ignore unreachable hosts and continue with play. This does not affect other task errors (see :term:`ignore_errors`) but is useful for groups of volatile/ephemeral hosts.
|
||||
loop: "Takes a list for the task to iterate over, saving each list element into the ``item`` variable (configurable via loop_control)"
|
||||
loop_control: |
|
||||
Several keys here allow you to modify/set loop behaviour in a task.
|
||||
|
|
|
@ -574,6 +574,7 @@ class Base(FieldAttributeBase):
|
|||
_no_log = FieldAttribute(isa='bool')
|
||||
_run_once = FieldAttribute(isa='bool')
|
||||
_ignore_errors = FieldAttribute(isa='bool')
|
||||
_ignore_unreachable = FieldAttribute(isa='bool')
|
||||
_check_mode = FieldAttribute(isa='bool')
|
||||
_diff = FieldAttribute(isa='bool')
|
||||
_any_errors_fatal = FieldAttribute(isa='bool')
|
||||
|
|
|
@ -511,8 +511,13 @@ class StrategyBase:
|
|||
self._tqm._stats.increment('changed', original_host.name)
|
||||
self._tqm.send_callback('v2_runner_on_failed', task_result, ignore_errors=ignore_errors)
|
||||
elif task_result.is_unreachable():
|
||||
self._tqm._unreachable_hosts[original_host.name] = True
|
||||
iterator._play._removed_hosts.append(original_host.name)
|
||||
ignore_unreachable = original_task.ignore_unreachable
|
||||
if not ignore_unreachable:
|
||||
self._tqm._unreachable_hosts[original_host.name] = True
|
||||
iterator._play._removed_hosts.append(original_host.name)
|
||||
else:
|
||||
self._tqm._stats.increment('skipped', original_host.name)
|
||||
task_result._result['skip_reason'] = 'Host %s is unreachable' % original_host.name
|
||||
self._tqm._stats.increment('dark', original_host.name)
|
||||
self._tqm.send_callback('v2_runner_on_unreachable', task_result)
|
||||
elif task_result.is_skipped():
|
||||
|
|
|
@ -45,6 +45,18 @@ def success(result):
|
|||
return not failed(result)
|
||||
|
||||
|
||||
def unreachable(result):
|
||||
''' Test if task result yields unreachable '''
|
||||
if not isinstance(result, MutableMapping):
|
||||
raise errors.AnsibleFilterError("The 'unreachable' test expects a dictionary")
|
||||
return result.get('unreachable', False)
|
||||
|
||||
|
||||
def reachable(result):
|
||||
''' Test if task result yields reachable '''
|
||||
return not unreachable(result)
|
||||
|
||||
|
||||
def changed(result):
|
||||
''' Test if task result yields changed '''
|
||||
if not isinstance(result, MutableMapping):
|
||||
|
@ -150,6 +162,8 @@ class TestModule(object):
|
|||
'succeeded': success,
|
||||
'success': success,
|
||||
'successful': success,
|
||||
'reachable': reachable,
|
||||
'unreachable': unreachable,
|
||||
|
||||
# changed testing
|
||||
'changed': changed,
|
||||
|
|
1
test/integration/targets/ignore_unreachable/aliases
Normal file
1
test/integration/targets/ignore_unreachable/aliases
Normal file
|
@ -0,0 +1 @@
|
|||
shippable/posix/group3
|
|
@ -0,0 +1,11 @@
|
|||
import ansible.plugins.connection.local as ansible_local
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
|
||||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
|
||||
class Connection(ansible_local.Connection):
|
||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
||||
display.debug('Intercepted call to exec remote command')
|
||||
raise AnsibleConnectionFailure('BADLOCAL Error: this is supposed to fail')
|
|
@ -0,0 +1,11 @@
|
|||
import ansible.plugins.connection.local as ansible_local
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
|
||||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
|
||||
class Connection(ansible_local.Connection):
|
||||
def put_file(self, in_path, out_path):
|
||||
display.debug('Intercepted call to send data')
|
||||
raise AnsibleConnectionFailure('BADLOCAL Error: this is supposed to fail')
|
3
test/integration/targets/ignore_unreachable/inventory
Normal file
3
test/integration/targets/ignore_unreachable/inventory
Normal file
|
@ -0,0 +1,3 @@
|
|||
nonexistent ansible_host=169.254.199.200
|
||||
bad_put_file ansible_host=localhost ansible_connection=bad_put_file
|
||||
bad_exec ansible_host=localhost ansible_connection=bad_exec
|
|
@ -0,0 +1,2 @@
|
|||
dependencies:
|
||||
- prepare_tests
|
16
test/integration/targets/ignore_unreachable/runme.sh
Executable file
16
test/integration/targets/ignore_unreachable/runme.sh
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
set -eux
|
||||
|
||||
export ANSIBLE_CONNECTION_PLUGINS=./fake_connectors
|
||||
# use fake connectors that raise srrors at different stages
|
||||
ansible-playbook test_with_bad_plugins.yml -i inventory -v "$@"
|
||||
unset ANSIBLE_CONNECTION_PLUGINS
|
||||
|
||||
ansible-playbook test_cannot_connect.yml -i inventory -v "$@"
|
||||
|
||||
if ansible-playbook test_base_cannot_connect.yml -i inventory -v "$@"; then
|
||||
echo "Playbook intended to fail succeeded. Connection succeeded to nonexistent host"
|
||||
exit 99
|
||||
else
|
||||
echo "Connection to nonexistent hosts failed without using ignore_unreachable. Success!"
|
||||
fi
|
|
@ -0,0 +1,5 @@
|
|||
- hosts: [localhost, nonexistent]
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Hi
|
||||
ping:
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Hi
|
||||
ping:
|
||||
- hosts: [localhost, nonexistent]
|
||||
ignore_unreachable: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Hi
|
||||
ping:
|
||||
- hosts: nonexistent
|
||||
ignore_unreachable: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Hi
|
||||
ping:
|
||||
- name: This should print anyway
|
||||
debug:
|
||||
msg: This should print worked even though host was unreachable
|
||||
- name: Hi
|
||||
ping:
|
||||
register: should_fail
|
||||
- assert:
|
||||
that:
|
||||
- 'should_fail is unreachable'
|
||||
- 'not (should_fail is skipped)'
|
||||
- 'not (should_fail is failed)'
|
|
@ -0,0 +1,24 @@
|
|||
- hosts: bad_put_file
|
||||
gather_facts: false
|
||||
ignore_unreachable: true
|
||||
tasks:
|
||||
- name: Hi
|
||||
ping:
|
||||
- hosts: bad_put_file
|
||||
gather_facts: true
|
||||
ignore_unreachable: true
|
||||
tasks:
|
||||
- name: Hi
|
||||
ping:
|
||||
- hosts: bad_exec
|
||||
gather_facts: false
|
||||
ignore_unreachable: true
|
||||
tasks:
|
||||
- name: Hi
|
||||
ping:
|
||||
- hosts: bad_exec
|
||||
gather_facts: true
|
||||
ignore_unreachable: true
|
||||
tasks:
|
||||
- name: Hi
|
||||
ping:
|
|
@ -269,6 +269,7 @@ class TestStrategyBase(unittest.TestCase):
|
|||
mock_task._role = None
|
||||
mock_task._parent = None
|
||||
mock_task.ignore_errors = False
|
||||
mock_task.ignore_unreachable = False
|
||||
mock_task._uuid = uuid.uuid4()
|
||||
mock_task.loop = None
|
||||
mock_task.copy.return_value = mock_task
|
||||
|
|
Loading…
Reference in a new issue