From 54787c07f35ebe3ea92d30d8df29450001bce0c5 Mon Sep 17 00:00:00 2001 From: Abhijeet Kasurde Date: Sat, 18 Jul 2020 02:17:05 +0530 Subject: [PATCH] [2.9] Fix json callback for non-lockstep strategy plugins such as free. (#70163) Fixes: #65931 Backport of https://github.com/ansible-collections/ansible.posix/pull/8 --- ...5931-json-callback-non-lockstep-output.yml | 4 ++ lib/ansible/plugins/callback/json.py | 53 ++++++++++++++++--- 2 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 changelogs/fragments/65931-json-callback-non-lockstep-output.yml diff --git a/changelogs/fragments/65931-json-callback-non-lockstep-output.yml b/changelogs/fragments/65931-json-callback-non-lockstep-output.yml new file mode 100644 index 00000000000..c6854ae21e2 --- /dev/null +++ b/changelogs/fragments/65931-json-callback-non-lockstep-output.yml @@ -0,0 +1,4 @@ +bugfixes: +- json callback - Fix host result to task references in the resultant JSON + output for non-lockstep strategy plugins such as free + (https://github.com/ansible/ansible/issues/65931) diff --git a/lib/ansible/plugins/callback/json.py b/lib/ansible/plugins/callback/json.py index 0f24c505bba..00285deb218 100644 --- a/lib/ansible/plugins/callback/json.py +++ b/lib/ansible/plugins/callback/json.py @@ -27,6 +27,11 @@ DOCUMENTATION = ''' - key: show_custom_stats section: defaults type: bool + notes: + - When using a strategy such as free, host_pinned, or a custom strategy, host results will + be added to new task results in ``.plays[].tasks[]``. As such, there will exist duplicate + task objects indicated by duplicate task IDs at ``.plays[].tasks[].task.id``, each with an + individual host result for the task. ''' import datetime @@ -35,10 +40,14 @@ import json from functools import partial from ansible.inventory.host import Host +from ansible.module_utils._text import to_text from ansible.parsing.ajson import AnsibleJSONEncoder from ansible.plugins.callback import CallbackBase +LOCKSTEP_CALLBACKS = frozenset(('linear', 'debug')) + + def current_time(): return '%sZ' % datetime.datetime.utcnow().isoformat() @@ -51,12 +60,15 @@ class CallbackModule(CallbackBase): def __init__(self, display=None): super(CallbackModule, self).__init__(display) self.results = [] + self._task_map = {} + self._is_lockstep = False def _new_play(self, play): + self._is_lockstep = play.strategy in LOCKSTEP_CALLBACKS return { 'play': { 'name': play.get_name(), - 'id': str(play._uuid), + 'id': to_text(play._uuid), 'duration': { 'start': current_time() } @@ -68,7 +80,7 @@ class CallbackModule(CallbackBase): return { 'task': { 'name': task.get_name(), - 'id': str(task._uuid), + 'id': to_text(task._uuid), 'duration': { 'start': current_time() } @@ -76,13 +88,32 @@ class CallbackModule(CallbackBase): 'hosts': {} } + def _find_result_task(self, host, task): + key = (host.get_name(), task._uuid) + return self._task_map.get( + key, + self.results[-1]['tasks'][-1] + ) + def v2_playbook_on_play_start(self, play): self.results.append(self._new_play(play)) + def v2_runner_on_start(self, host, task): + if self._is_lockstep: + return + key = (host.get_name(), task._uuid) + task_result = self._new_task(task) + self._task_map[key] = task_result + self.results[-1]['tasks'].append(task_result) + def v2_playbook_on_task_start(self, task, is_conditional): + if not self._is_lockstep: + return self.results[-1]['tasks'].append(self._new_task(task)) def v2_playbook_on_handler_task_start(self, task): + if not self._is_lockstep: + return self.results[-1]['tasks'].append(self._new_task(task)) def _convert_host_to_name(self, key): @@ -120,14 +151,22 @@ class CallbackModule(CallbackBase): """This function is used as a partial to add failed/skipped info in a single method""" host = result._host task = result._task - task_result = result._result.copy() - task_result.update(on_info) - task_result['action'] = task.action - self.results[-1]['tasks'][-1]['hosts'][host.name] = task_result + + result_copy = result._result.copy() + result_copy.update(on_info) + result_copy['action'] = task.action + + task_result = self._find_result_task(host, task) + + task_result['hosts'][host.name] = result_copy end_time = current_time() - self.results[-1]['tasks'][-1]['task']['duration']['end'] = end_time + task_result['task']['duration']['end'] = end_time self.results[-1]['play']['duration']['end'] = end_time + if not self._is_lockstep: + key = (host.get_name(), task._uuid) + del self._task_map[key] + def __getattribute__(self, name): """Return ``_record_task_result`` partial with a dict containing skipped/failed if necessary""" if name not in ('v2_runner_on_ok', 'v2_runner_on_failed', 'v2_runner_on_unreachable', 'v2_runner_on_skipped'):