diff --git a/changelogs/fragments/74953-implement-async-callbacks.yml b/changelogs/fragments/74953-implement-async-callbacks.yml new file mode 100644 index 00000000000..4602c136f17 --- /dev/null +++ b/changelogs/fragments/74953-implement-async-callbacks.yml @@ -0,0 +1,4 @@ +minor_changes: + - callback API - implemented ``v2_runner_on_async_ok`` and ``v2_runner_on_async_failed`` callbacks + (https://github.com/ansible/ansible/pull/74953). + - default callback plugin - displays output for ``v2_runner_on_async_ok`` and ``v2_runner_on_async_failed`` callbacks. diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index 4891c0772f8..fe54b559a1c 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -617,6 +617,20 @@ class TaskExecutor: if self._task.async_val > 0: if self._task.poll > 0 and not result.get('skipped') and not result.get('failed'): result = self._poll_async_result(result=result, templar=templar, task_vars=vars_copy) + if result.get('failed'): + self._final_q.send_callback( + 'v2_runner_on_async_failed', + TaskResult(self._host.name, + self._task, # We send the full task here, because the controller knows nothing about it, the TE created it + result, + task_fields=self._task.dump_attrs())) + else: + self._final_q.send_callback( + 'v2_runner_on_async_ok', + TaskResult(self._host.name, + self._task, # We send the full task here, because the controller knows nothing about it, the TE created it + result, + task_fields=self._task.dump_attrs())) # ensure no log is preserved result["_ansible_no_log"] = self._play_context.no_log @@ -831,7 +845,7 @@ class TaskExecutor: if int(async_result.get('finished', 0)) != 1: if async_result.get('_ansible_parsed'): - return dict(failed=True, msg="async task did not complete within the requested time - %ss" % self._task.async_val) + return dict(failed=True, msg="async task did not complete within the requested time - %ss" % self._task.async_val, async_result=async_result) else: return dict(failed=True, msg="async task produced unparseable results", async_result=async_result) else: diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py index a1adf3b9ee1..38a1daa76df 100644 --- a/lib/ansible/plugins/callback/__init__.py +++ b/lib/ansible/plugins/callback/__init__.py @@ -364,7 +364,6 @@ class CallbackBase(AnsiblePlugin): host = result._host.get_name() self.runner_on_unreachable(host, result._result) - # FIXME: not called def v2_runner_on_async_poll(self, result): host = result._host.get_name() jid = result._result.get('ansible_job_id') @@ -372,16 +371,18 @@ class CallbackBase(AnsiblePlugin): clock = 0 self.runner_on_async_poll(host, result._result, jid, clock) - # FIXME: not called def v2_runner_on_async_ok(self, result): host = result._host.get_name() jid = result._result.get('ansible_job_id') self.runner_on_async_ok(host, result._result, jid) - # FIXME: not called def v2_runner_on_async_failed(self, result): host = result._host.get_name() + # Attempt to get the async job ID. If the job does not finish before the + # async timeout value, the ID may be within the unparsed 'async_result' dict. jid = result._result.get('ansible_job_id') + if not jid and 'async_result' in result._result: + jid = result._result['async_result'].get('ansible_job_id') self.runner_on_async_failed(host, result._result, jid) def v2_playbook_on_start(self, playbook): diff --git a/lib/ansible/plugins/callback/default.py b/lib/ansible/plugins/callback/default.py index 8aca378b149..a65dcd8e7cc 100644 --- a/lib/ansible/plugins/callback/default.py +++ b/lib/ansible/plugins/callback/default.py @@ -413,6 +413,21 @@ class CallbackModule(CallbackBase): color=C.COLOR_DEBUG ) + def v2_runner_on_async_ok(self, result): + host = result._host.get_name() + jid = result._result.get('ansible_job_id') + self._display.display("ASYNC OK on %s: jid=%s" % (host, jid), color=C.COLOR_DEBUG) + + def v2_runner_on_async_failed(self, result): + host = result._host.get_name() + + # Attempt to get the async job ID. If the job does not finish before the + # async timeout value, the ID may be within the unparsed 'async_result' dict. + jid = result._result.get('ansible_job_id') + if not jid and 'async_result' in result._result: + jid = result._result['async_result'].get('ansible_job_id') + self._display.display("ASYNC FAILED on %s: jid=%s" % (host, jid), color=C.COLOR_DEBUG) + def v2_playbook_on_notify(self, handler, host): if self._display.verbosity > 1: self._display.display("NOTIFIED HANDLER %s for %s" % (handler.get_name(), host), color=C.COLOR_VERBOSE, screen_only=True) diff --git a/test/integration/targets/callback_default/runme.sh b/test/integration/targets/callback_default/runme.sh index b5c98ef72b8..f9b60b6ba3c 100755 --- a/test/integration/targets/callback_default/runme.sh +++ b/test/integration/targets/callback_default/runme.sh @@ -125,6 +125,13 @@ export ANSIBLE_CHECK_MODE_MARKERS=0 run_test default +# Check for async output +# NOTE: regex to match 1 or more digits works for both BSD and GNU grep +ansible-playbook -i inventory test_async.yml 2>&1 | tee async_test.out +grep "ASYNC OK .* jid=[0-9]\{1,\}" async_test.out +grep "ASYNC FAILED .* jid=[0-9]\{1,\}" async_test.out +rm -f async_test.out + # Hide skipped export ANSIBLE_DISPLAY_SKIPPED_HOSTS=0 diff --git a/test/integration/targets/callback_default/test_async.yml b/test/integration/targets/callback_default/test_async.yml new file mode 100644 index 00000000000..57294a4c005 --- /dev/null +++ b/test/integration/targets/callback_default/test_async.yml @@ -0,0 +1,14 @@ +--- +- hosts: testhost + gather_facts: no + tasks: + - name: test success async output + command: sleep 1 + async: 10 + poll: 1 + + - name: test failure async output + command: sleep 10 + async: 1 + poll: 1 + ignore_errors: yes