diff --git a/changelogs/fragments/83292.yml b/changelogs/fragments/83292.yml new file mode 100644 index 00000000000..6720247aaea --- /dev/null +++ b/changelogs/fragments/83292.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - Fix the issue of playbook with any_errors_fatal as true, the rescue section only executes on the host where a fail task with run_once as true is triggered, not affecting other hosts as expected.(https://github.com/ansible/ansible/issues/83292) \ No newline at end of file diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index 39b3851125a..1fca89140af 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -562,7 +562,7 @@ class StrategyBase: # save the current state before failing it for later inspection state_when_failed = iterator.get_state_for_host(original_host.name) display.debug("marking %s as failed" % original_host.name) - if original_task.run_once: + if original_task.run_once and not original_task.any_errors_fatal: # if we're using run_once, we have to fail every host here for h in self._inventory.get_hosts(iterator._play.hosts): if h.name not in self._tqm._unreachable_hosts: diff --git a/lib/ansible/plugins/strategy/linear.py b/lib/ansible/plugins/strategy/linear.py index 7415e1bdbe3..994eecd0581 100644 --- a/lib/ansible/plugins/strategy/linear.py +++ b/lib/ansible/plugins/strategy/linear.py @@ -166,7 +166,7 @@ class StrategyModule(StrategyBase): results.extend(self._execute_meta(task, play_context, iterator, host)) if task._get_meta() not in ('noop', 'reset_connection', 'end_host', 'role_complete', 'flush_handlers', 'end_role'): run_once = True - if (task.any_errors_fatal or run_once) and not task.ignore_errors: + if task.any_errors_fatal and not task.ignore_errors: any_errors_fatal = True else: # handle step if needed, skip meta actions as they are used internally @@ -179,7 +179,7 @@ class StrategyModule(StrategyBase): run_once = action and getattr(action, 'BYPASS_HOST_LOOP', False) or templar.template(task.run_once) - if (task.any_errors_fatal or run_once) and not task.ignore_errors: + if task.any_errors_fatal and not task.ignore_errors: any_errors_fatal = True if not callback_sent: diff --git a/test/integration/targets/any_errors_fatal/83292.yml b/test/integration/targets/any_errors_fatal/83292.yml new file mode 100644 index 00000000000..179cbeef5b7 --- /dev/null +++ b/test/integration/targets/any_errors_fatal/83292.yml @@ -0,0 +1,19 @@ +- hosts: testhost,testhost2 + gather_facts: false + any_errors_fatal: "{{ any_errors_fatal | default(omit) }}" + tasks: + - block: + - debug: + msg: Some task running for all hosts + - fail: + run_once: "{{ run_once | default(omit) }}" + - name: any_errors_fatal fails all hosts when any of them fails + debug: + msg: SHOULD NOT HAPPEN + rescue: + - name: Rescues both hosts + debug: + msg: rescuedd + - name: You can recover from fatal errors by adding a rescue section to the block. + debug: + msg: recovered diff --git a/test/integration/targets/any_errors_fatal/runme.sh b/test/integration/targets/any_errors_fatal/runme.sh index 55381a73679..12d487f7679 100755 --- a/test/integration/targets/any_errors_fatal/runme.sh +++ b/test/integration/targets/any_errors_fatal/runme.sh @@ -47,3 +47,21 @@ ansible-playbook -i inventory "$@" 80981.yml | tee out.txt [ "$(grep -c 'SHOULD NOT HAPPEN' out.txt)" -eq 0 ] [ "$(grep -c 'rescuedd' out.txt)" -eq 2 ] [ "$(grep -c 'recovered' out.txt)" -eq 2 ] + +args=( + "-e any_errors_fatal=true" + "-e run_once=true" + "-e any_errors_fatal=true run_once=true" + "" +) +for arg in "${args[@]}"; do + if [ -z "$arg" ]; then + ansible-playbook -i inventory "$@" 83292.yml | tee out.txt + else + ansible-playbook -i inventory "$@" 83292.yml "$arg" | tee out.txt + fi + + [ "$(grep -c 'SHOULD NOT HAPPEN' out.txt)" -eq 0 ] + [ "$(grep -c 'rescuedd' out.txt)" -eq 2 ] + [ "$(grep -c 'recovered' out.txt)" -eq 2 ] +done