New keyword: ignore_unreachable (#43857)

pull/44575/head
jctanner 6 years ago committed by Ryan Brown
parent 2603604fd6
commit 653d9c0f87

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

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

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