diff --git a/changelogs/fragments/12067-counter-enabled-loop-output.yml b/changelogs/fragments/12067-counter-enabled-loop-output.yml new file mode 100644 index 0000000000..a97ec38fea --- /dev/null +++ b/changelogs/fragments/12067-counter-enabled-loop-output.yml @@ -0,0 +1,2 @@ +bugfixes: + - counter_enabled callback - fix missing output for looped tasks, including tasks using ``delegate_to`` (https://github.com/ansible-collections/community.general/issues/8187, https://github.com/ansible-collections/community.general/pull/12067). diff --git a/plugins/callback/counter_enabled.py b/plugins/callback/counter_enabled.py index f6a961bb09..c543868485 100644 --- a/plugins/callback/counter_enabled.py +++ b/plugins/callback/counter_enabled.py @@ -140,69 +140,83 @@ class CallbackModule(CallbackBase): self._host_counter = self._previous_batch_total self._task_counter += 1 - def v2_runner_on_ok(self, result): - self._host_counter += 1 - - delegated_vars = result._result.get("_ansible_delegated_vars", None) - - if self._play.strategy == "free" and self._last_task_banner != result._task._uuid: - self._print_task_banner(result._task) + def _host_label(self, result): + host_name = result._host.get_name() + delegate_to = result._task.delegate_to + if delegate_to and delegate_to != host_name: + delegated_vars = result._result.get("_ansible_delegated_vars", {}) + ahost = delegated_vars.get("ansible_host", delegate_to) + label = f"{host_name} -> {delegate_to}" + if ahost != delegate_to: + label += f"({ahost})" + return f"[{label}]" + return f"[{host_name}]" + def _display_result_ok(self, result, counter_str="", item_suffix=""): if isinstance(result._task, TaskInclude): return elif result._result.get("changed", False): - if delegated_vars: - msg = f"changed: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> {delegated_vars['ansible_host']}]" - else: - msg = f"changed: {self._host_counter}/{self._host_total} [{result._host.get_name()}]" + msg = f"changed: {counter_str}{self._host_label(result)}{item_suffix}" color = C.COLOR_CHANGED else: if not self._plugin_options.get("display_ok_hosts", True): return - if delegated_vars: - msg = f"ok: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> {delegated_vars['ansible_host']}]" - else: - msg = f"ok: {self._host_counter}/{self._host_total} [{result._host.get_name()}]" + msg = f"ok: {counter_str}{self._host_label(result)}{item_suffix}" color = C.COLOR_OK self._handle_warnings(result._result) + self._clean_results(result._result, result._task.action) + if self._run_is_verbose(result): + msg += f" => {self._dump_results(result._result)}" + self._display.display(msg, color=color) + + def v2_runner_on_ok(self, result): + self._host_counter += 1 + + if self._play.strategy == "free" and self._last_task_banner != result._task._uuid: + self._print_task_banner(result._task) if result._task.loop and "results" in result._result: self._process_items(result) else: - self._clean_results(result._result, result._task.action) + self._display_result_ok(result, f"{self._host_counter}/{self._host_total} ") - if self._run_is_verbose(result): - msg += f" => {self._dump_results(result._result)}" - self._display.display(msg, color=color) + def v2_runner_item_on_ok(self, result): + self._display_result_ok(result, item_suffix=f" => (item={self._get_item_label(result._result)})") + + def _display_result_failed(self, result, counter_str="", item_suffix=""): + self._clean_results(result._result, result._task.action) + self._handle_warnings(result._result) + prefix = "failed" if item_suffix else "fatal" + msg = f"{prefix}: {counter_str}{self._host_label(result)}{item_suffix}" + self._display.display(f"{msg} => {self._dump_results(result._result)}", color=C.COLOR_ERROR) + + def v2_runner_item_on_failed(self, result): + self._display_result_failed(result, item_suffix=f" (item={self._get_item_label(result._result)})") + + def _display_result_skipped(self, result, counter_str="", item_suffix=""): + self._clean_results(result._result, result._task.action) + msg = f"skipping: {counter_str}[{result._host.get_name()}]{item_suffix}" + if self._run_is_verbose(result): + msg += f" => {self._dump_results(result._result)}" + self._display.display(msg, color=C.COLOR_SKIP) + + def v2_runner_item_on_skipped(self, result): + if self._plugin_options.get("show_skipped_hosts", C.DISPLAY_SKIPPED_HOSTS): + self._display_result_skipped(result, item_suffix=f" => (item={self._get_item_label(result._result)})") def v2_runner_on_failed(self, result, ignore_errors=False): self._host_counter += 1 - delegated_vars = result._result.get("_ansible_delegated_vars", None) - self._clean_results(result._result, result._task.action) - if self._play.strategy == "free" and self._last_task_banner != result._task._uuid: self._print_task_banner(result._task) self._handle_exception(result._result) - self._handle_warnings(result._result) if result._task.loop and "results" in result._result: self._process_items(result) - else: - if delegated_vars: - self._display.display( - f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> " - f"{delegated_vars['ansible_host']}]: FAILED! => {self._dump_results(result._result)}", - color=C.COLOR_ERROR, - ) - else: - self._display.display( - f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()}]: FAILED! => {self._dump_results(result._result)}", - color=C.COLOR_ERROR, - ) + self._display_result_failed(result, f"{self._host_counter}/{self._host_total} ", ": FAILED!") if ignore_errors: self._display.display("...ignoring", color=C.COLOR_SKIP) @@ -210,21 +224,14 @@ class CallbackModule(CallbackBase): def v2_runner_on_skipped(self, result): self._host_counter += 1 - if self._plugin_options.get( - "show_skipped_hosts", C.DISPLAY_SKIPPED_HOSTS - ): # fallback on constants for inherited plugins missing docs - self._clean_results(result._result, result._task.action) - + if self._plugin_options.get("show_skipped_hosts", C.DISPLAY_SKIPPED_HOSTS): if self._play.strategy == "free" and self._last_task_banner != result._task._uuid: self._print_task_banner(result._task) if result._task.loop and "results" in result._result: self._process_items(result) else: - msg = f"skipping: {self._host_counter}/{self._host_total} [{result._host.get_name()}]" - if self._run_is_verbose(result): - msg += f" => {self._dump_results(result._result)}" - self._display.display(msg, color=C.COLOR_SKIP) + self._display_result_skipped(result, f"{self._host_counter}/{self._host_total} ") def v2_runner_on_unreachable(self, result): self._host_counter += 1 @@ -232,15 +239,7 @@ class CallbackModule(CallbackBase): if self._play.strategy == "free" and self._last_task_banner != result._task._uuid: self._print_task_banner(result._task) - delegated_vars = result._result.get("_ansible_delegated_vars", None) - if delegated_vars: - self._display.display( - f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()} -> " - f"{delegated_vars['ansible_host']}]: UNREACHABLE! => {self._dump_results(result._result)}", - color=C.COLOR_UNREACHABLE, - ) - else: - self._display.display( - f"fatal: {self._host_counter}/{self._host_total} [{result._host.get_name()}]: UNREACHABLE! => {self._dump_results(result._result)}", - color=C.COLOR_UNREACHABLE, - ) + self._display.display( + f"fatal: {self._host_counter}/{self._host_total} {self._host_label(result)}: UNREACHABLE! => {self._dump_results(result._result)}", + color=C.COLOR_UNREACHABLE, + ) diff --git a/tests/integration/targets/callback_counter_enabled/aliases b/tests/integration/targets/callback_counter_enabled/aliases new file mode 100644 index 0000000000..3e2dd244c1 --- /dev/null +++ b/tests/integration/targets/callback_counter_enabled/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/posix/3 +needs/target/callback diff --git a/tests/integration/targets/callback_counter_enabled/tasks/main.yml b/tests/integration/targets/callback_counter_enabled/tasks/main.yml new file mode 100644 index 0000000000..7e824cbfb3 --- /dev/null +++ b/tests/integration/targets/callback_counter_enabled/tasks/main.yml @@ -0,0 +1,102 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Run tests + include_role: + name: callback + vars: + tests: + - name: Task and host counters shown in N/M format + environment: + ANSIBLE_NOCOLOR: 'true' + ANSIBLE_FORCE_COLOR: 'false' + ANSIBLE_STDOUT_CALLBACK: community.general.counter_enabled + playbook: | + - hosts: testhost + gather_facts: false + tasks: + - name: First task + set_fact: + counter_test: first + - name: Second task + set_fact: + counter_test: second + expected_output: + - "" + - "PLAY [testhost] ****************************************************************" + - "" + - "TASK 1/2 [First task] **********************************************************" + - "ok: 1/1 [testhost]" + - "" + - "TASK 2/2 [Second task] *********************************************************" + - "ok: 1/1 [testhost]" + - "" + - "PLAY RECAP *********************************************************************" + - "testhost : ok=2 changed=0 unreachable=0 failed=0 rescued=0 ignored=0 " + + - name: Loop task items are displayed (regression for issue 8187) + environment: + ANSIBLE_NOCOLOR: 'true' + ANSIBLE_FORCE_COLOR: 'false' + ANSIBLE_STDOUT_CALLBACK: community.general.counter_enabled + playbook: !unsafe | + - hosts: testhost + gather_facts: false + tasks: + - name: Loop task + set_fact: + dummy_fact: "{{ item }}" + loop: + - item_a + - item_b + - item_c + expected_output: + - "" + - "PLAY [testhost] ****************************************************************" + - "" + - "TASK 1/1 [Loop task] ***********************************************************" + - "ok: [testhost] => (item=item_a)" + - "ok: [testhost] => (item=item_b)" + - "ok: [testhost] => (item=item_c)" + - "" + - "PLAY RECAP *********************************************************************" + - "testhost : ok=1 changed=0 unreachable=0 failed=0 rescued=0 ignored=0 " + + - name: Delegated loop task items show delegation target (regression for issue 8187) + environment: + ANSIBLE_NOCOLOR: 'true' + ANSIBLE_FORCE_COLOR: 'false' + ANSIBLE_STDOUT_CALLBACK: community.general.counter_enabled + playbook: !unsafe | + - hosts: testhost + gather_facts: false + tasks: + - name: Delegated loop task + set_fact: + dummy_fact: "{{ item }}" + loop: + - item_a + - item_b + - item_c + delegate_to: localhost + expected_output: + - "" + - "PLAY [testhost] ****************************************************************" + - "" + - "TASK 1/1 [Delegated loop task] *************************************************" + - - "ok: [testhost -> localhost] => (item=item_a)" + - "ok: [testhost -> localhost(127.0.0.1)] => (item=item_a)" + - - "ok: [testhost -> localhost] => (item=item_b)" + - "ok: [testhost -> localhost(127.0.0.1)] => (item=item_b)" + - - "ok: [testhost -> localhost] => (item=item_c)" + - "ok: [testhost -> localhost(127.0.0.1)] => (item=item_c)" + - "" + - "PLAY RECAP *********************************************************************" + - "testhost : ok=1 changed=0 unreachable=0 failed=0 rescued=0 ignored=0 "