From 653d9c0f87f681ac386864bad4cb48f0c5e2ddfe Mon Sep 17 00:00:00 2001 From: jctanner Date: Thu, 23 Aug 2018 11:41:02 -0400 Subject: [PATCH] New keyword: ignore_unreachable (#43857) --- changelogs/fragments/ignore_unreachable.yml | 2 ++ docs/docsite/keyword_desc.yml | 1 + lib/ansible/playbook/base.py | 1 + lib/ansible/plugins/strategy/__init__.py | 9 ++++-- lib/ansible/plugins/test/core.py | 14 +++++++++ .../targets/ignore_unreachable/aliases | 1 + .../fake_connectors/bad_exec.py | 11 +++++++ .../fake_connectors/bad_put_file.py | 11 +++++++ .../targets/ignore_unreachable/inventory | 3 ++ .../targets/ignore_unreachable/meta/main.yml | 2 ++ .../targets/ignore_unreachable/runme.sh | 16 ++++++++++ .../test_base_cannot_connect.yml | 5 ++++ .../test_cannot_connect.yml | 30 +++++++++++++++++++ .../test_with_bad_plugins.yml | 24 +++++++++++++++ .../plugins/strategy/test_strategy_base.py | 1 + 15 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/ignore_unreachable.yml create mode 100644 test/integration/targets/ignore_unreachable/aliases create mode 100644 test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py create mode 100644 test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py create mode 100644 test/integration/targets/ignore_unreachable/inventory create mode 100644 test/integration/targets/ignore_unreachable/meta/main.yml create mode 100755 test/integration/targets/ignore_unreachable/runme.sh create mode 100644 test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml create mode 100644 test/integration/targets/ignore_unreachable/test_cannot_connect.yml create mode 100644 test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml diff --git a/changelogs/fragments/ignore_unreachable.yml b/changelogs/fragments/ignore_unreachable.yml new file mode 100644 index 0000000000..b514c42d25 --- /dev/null +++ b/changelogs/fragments/ignore_unreachable.yml @@ -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. diff --git a/docs/docsite/keyword_desc.yml b/docs/docsite/keyword_desc.yml index 73049118d6..5203b6791d 100644 --- a/docs/docsite/keyword_desc.yml +++ b/docs/docsite/keyword_desc.yml @@ -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. diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py index 6ac2c86c47..84c3fee1c1 100644 --- a/lib/ansible/playbook/base.py +++ b/lib/ansible/playbook/base.py @@ -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') diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index 27862186ee..cb21bb9544 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -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(): diff --git a/lib/ansible/plugins/test/core.py b/lib/ansible/plugins/test/core.py index 4635736906..aab2b5aa3f 100644 --- a/lib/ansible/plugins/test/core.py +++ b/lib/ansible/plugins/test/core.py @@ -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, diff --git a/test/integration/targets/ignore_unreachable/aliases b/test/integration/targets/ignore_unreachable/aliases new file mode 100644 index 0000000000..b59832142f --- /dev/null +++ b/test/integration/targets/ignore_unreachable/aliases @@ -0,0 +1 @@ +shippable/posix/group3 diff --git a/test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py b/test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py new file mode 100644 index 0000000000..b5e9ca88a6 --- /dev/null +++ b/test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py @@ -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') diff --git a/test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py b/test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py new file mode 100644 index 0000000000..98927997a1 --- /dev/null +++ b/test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py @@ -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') diff --git a/test/integration/targets/ignore_unreachable/inventory b/test/integration/targets/ignore_unreachable/inventory new file mode 100644 index 0000000000..495a68cfb2 --- /dev/null +++ b/test/integration/targets/ignore_unreachable/inventory @@ -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 diff --git a/test/integration/targets/ignore_unreachable/meta/main.yml b/test/integration/targets/ignore_unreachable/meta/main.yml new file mode 100644 index 0000000000..07faa21776 --- /dev/null +++ b/test/integration/targets/ignore_unreachable/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_tests diff --git a/test/integration/targets/ignore_unreachable/runme.sh b/test/integration/targets/ignore_unreachable/runme.sh new file mode 100755 index 0000000000..5b0ef190ef --- /dev/null +++ b/test/integration/targets/ignore_unreachable/runme.sh @@ -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 diff --git a/test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml b/test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml new file mode 100644 index 0000000000..931c82bf3a --- /dev/null +++ b/test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml @@ -0,0 +1,5 @@ +- hosts: [localhost, nonexistent] + gather_facts: false + tasks: + - name: Hi + ping: diff --git a/test/integration/targets/ignore_unreachable/test_cannot_connect.yml b/test/integration/targets/ignore_unreachable/test_cannot_connect.yml new file mode 100644 index 0000000000..501a2cab72 --- /dev/null +++ b/test/integration/targets/ignore_unreachable/test_cannot_connect.yml @@ -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)' diff --git a/test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml b/test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml new file mode 100644 index 0000000000..5d62f19973 --- /dev/null +++ b/test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml @@ -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: diff --git a/test/units/plugins/strategy/test_strategy_base.py b/test/units/plugins/strategy/test_strategy_base.py index 8f3781e98d..909c86089b 100644 --- a/test/units/plugins/strategy/test_strategy_base.py +++ b/test/units/plugins/strategy/test_strategy_base.py @@ -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