Make it so callback plugins can act on implicit/explicit meta tasks (#71009)

Change:
- Now sends meta tasks to the task start callback
- Lets callback plugins opt-in to receiving implicit tasks

Test Plan:
- New integration tests

Tickets:
- Indirectly fixes #71007 by allowing custom callbacks with this data

Signed-off-by: Rick Elrod <rick@elrod.me>
pull/69107/head
Rick Elrod 4 years ago committed by GitHub
parent a1257d75aa
commit ea58d7c233
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
minor_changes:
- callback plugins - ``meta`` tasks now get sent to ``v2_playbook_on_task_start``. Explicit tasks are always sent. Plugins can opt in to receiving implicit ones.

@ -278,6 +278,10 @@ Note that the ``CALLBACK_VERSION`` and ``CALLBACK_NAME`` definitions are require
For example callback plugins, see the source code for the `callback plugins included with Ansible Core <https://github.com/ansible/ansible/tree/devel/lib/ansible/plugins/callback>`_ For example callback plugins, see the source code for the `callback plugins included with Ansible Core <https://github.com/ansible/ansible/tree/devel/lib/ansible/plugins/callback>`_
New in ansible-base 2.11, callback plugins are notified (via ``v2_playbook_on_task_start``) of :ref:`meta<meta_module>` tasks. By default, only explicit ``meta`` tasks that users list in their plays are sent to callbacks.
There are also some tasks which are generated internally and implicitly at various points in execution. Callback plugins can opt-in to receiving these implicit tasks as well, by setting ``self.wants_implicit_tasks = True``. Any ``Task`` object received by a callback hook will have an ``.implicit`` attribute, which can be consulted to determine whether the ``Task`` originated from within Ansible, or explicitly by the user.
.. _developing_connection_plugins: .. _developing_connection_plugins:
Connection plugins Connection plugins

@ -66,6 +66,7 @@ Plugins
======= =======
* inventory plugins - ``CachePluginAdjudicator.flush()`` now calls the underlying cache plugin's ``flush()`` instead of only deleting keys that it knows about. Inventory plugins should use ``delete()`` to remove any specific keys. As a user, this means that when an inventory plugin calls its ``clear_cache()`` method, facts could also be flushed from the cache. To work around this, users can configure inventory plugins to use a cache backend that is independent of the facts cache. * inventory plugins - ``CachePluginAdjudicator.flush()`` now calls the underlying cache plugin's ``flush()`` instead of only deleting keys that it knows about. Inventory plugins should use ``delete()`` to remove any specific keys. As a user, this means that when an inventory plugin calls its ``clear_cache()`` method, facts could also be flushed from the cache. To work around this, users can configure inventory plugins to use a cache backend that is independent of the facts cache.
* callback plugins - ``meta`` task execution is now sent to ``v2_playbook_on_task_start`` like any other task. By default, only explicit meta tasks are sent there. Callback plugins can opt-in to receiving internal, implicitly created tasks to act on those as well, as noted in the plugin development documentation.
Porting custom scripts Porting custom scripts
====================== ======================

@ -34,6 +34,7 @@ from ansible.executor.task_result import TaskResult
from ansible.module_utils.six import PY3, string_types from ansible.module_utils.six import PY3, string_types
from ansible.module_utils._text import to_text, to_native from ansible.module_utils._text import to_text, to_native
from ansible.playbook.play_context import PlayContext from ansible.playbook.play_context import PlayContext
from ansible.playbook.task import Task
from ansible.plugins.loader import callback_loader, strategy_loader, module_loader from ansible.plugins.loader import callback_loader, strategy_loader, module_loader
from ansible.plugins.callback import CallbackBase from ansible.plugins.callback import CallbackBase
from ansible.template import Templar from ansible.template import Templar
@ -359,6 +360,13 @@ class TaskQueueManager:
if getattr(callback_plugin, 'disabled', False): if getattr(callback_plugin, 'disabled', False):
continue continue
# a plugin can opt in to implicit tasks (such as meta). It does this
# by declaring self.wants_implicit_tasks = True.
wants_implicit_tasks = getattr(
callback_plugin,
'wants_implicit_tasks',
False)
# try to find v2 method, fallback to v1 method, ignore callback if no method found # try to find v2 method, fallback to v1 method, ignore callback if no method found
methods = [] methods = []
for possible in [method_name, 'v2_on_any']: for possible in [method_name, 'v2_on_any']:
@ -370,6 +378,12 @@ class TaskQueueManager:
# send clean copies # send clean copies
new_args = [] new_args = []
# If we end up being given an implicit task, we'll set this flag in
# the loop below. If the plugin doesn't care about those, then we
# check and continue to the next iteration of the outer loop.
is_implicit_task = False
for arg in args: for arg in args:
# FIXME: add play/task cleaners # FIXME: add play/task cleaners
if isinstance(arg, TaskResult): if isinstance(arg, TaskResult):
@ -379,6 +393,12 @@ class TaskQueueManager:
else: else:
new_args.append(arg) new_args.append(arg)
if isinstance(arg, Task) and arg.implicit:
is_implicit_task = True
if is_implicit_task and not wants_implicit_tasks:
continue
for method in methods: for method in methods:
try: try:
method(*new_args, **kwargs) method(*new_args, **kwargs)

@ -272,6 +272,9 @@ class Play(Base, Taggable, CollectionSearch):
loader=self._loader loader=self._loader
) )
for task in flush_block.block:
task.implicit = True
block_list = [] block_list = []
block_list.extend(self.pre_tasks) block_list.extend(self.pre_tasks)

@ -97,6 +97,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
self._role = role self._role = role
self._parent = None self._parent = None
self.implicit = False
if task_include: if task_include:
self._parent = task_include self._parent = task_include
@ -411,6 +412,8 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
if self._role: if self._role:
new_me._role = self._role new_me._role = self._role
new_me.implicit = self.implicit
return new_me return new_me
def serialize(self): def serialize(self):
@ -427,6 +430,8 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
if self._ansible_internal_redirect_list: if self._ansible_internal_redirect_list:
data['_ansible_internal_redirect_list'] = self._ansible_internal_redirect_list[:] data['_ansible_internal_redirect_list'] = self._ansible_internal_redirect_list[:]
data['implicit'] = self.implicit
return data return data
def deserialize(self, data): def deserialize(self, data):
@ -457,6 +462,8 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
self._ansible_internal_redirect_list = data.get('_ansible_internal_redirect_list', []) self._ansible_internal_redirect_list = data.get('_ansible_internal_redirect_list', [])
self.implicit = data.get('implicit', False)
super(Task, self).deserialize(data) super(Task, self).deserialize(data)
def set_loader(self, loader): def set_loader(self, loader):

@ -74,6 +74,7 @@ class CallbackBase(AnsiblePlugin):
self._display.vvvv('Loading callback plugin %s of type %s, v%s from %s' % (name, ctype, version, sys.modules[self.__module__].__file__)) self._display.vvvv('Loading callback plugin %s of type %s, v%s from %s' % (name, ctype, version, sys.modules[self.__module__].__file__))
self.disabled = False self.disabled = False
self.wants_implicit_tasks = False
self._plugin_options = {} self._plugin_options = {}
if options is not None: if options is not None:

@ -1138,22 +1138,20 @@ class StrategyBase:
skipped = False skipped = False
msg = '' msg = ''
# The top-level conditions should only compare meta_action self._tqm.send_callback('v2_playbook_on_task_start', task, is_conditional=False)
# These don't support "when" conditionals
if meta_action in ('noop', 'flush_handlers', 'refresh_inventory', 'reset_connection') and task.when:
self._cond_not_supported_warn(meta_action)
if meta_action == 'noop': if meta_action == 'noop':
# FIXME: issue a callback for the noop here?
if task.when:
self._cond_not_supported_warn(meta_action)
msg = "noop" msg = "noop"
elif meta_action == 'flush_handlers': elif meta_action == 'flush_handlers':
if task.when:
self._cond_not_supported_warn(meta_action)
self._flushed_hosts[target_host] = True self._flushed_hosts[target_host] = True
self.run_handlers(iterator, play_context) self.run_handlers(iterator, play_context)
self._flushed_hosts[target_host] = False self._flushed_hosts[target_host] = False
msg = "ran handlers" msg = "ran handlers"
elif meta_action == 'refresh_inventory': elif meta_action == 'refresh_inventory':
if task.when:
self._cond_not_supported_warn(meta_action)
self._inventory.refresh_inventory() self._inventory.refresh_inventory()
self._set_hosts_cache(iterator._play) self._set_hosts_cache(iterator._play)
msg = "inventory successfully refreshed" msg = "inventory successfully refreshed"
@ -1180,6 +1178,8 @@ class StrategyBase:
if host.name not in self._tqm._unreachable_hosts: if host.name not in self._tqm._unreachable_hosts:
iterator._host_states[host.name].run_state = iterator.ITERATING_COMPLETE iterator._host_states[host.name].run_state = iterator.ITERATING_COMPLETE
msg = "ending play" msg = "ending play"
else:
skipped = True
elif meta_action == 'end_host': elif meta_action == 'end_host':
if _evaluate_conditional(target_host): if _evaluate_conditional(target_host):
iterator._host_states[target_host.name].run_state = iterator.ITERATING_COMPLETE iterator._host_states[target_host.name].run_state = iterator.ITERATING_COMPLETE
@ -1211,9 +1211,6 @@ class StrategyBase:
# a certain subset of variables exist. # a certain subset of variables exist.
play_context.update_vars(all_vars) play_context.update_vars(all_vars)
if task.when:
self._cond_not_supported_warn(meta_action)
if target_host in self._active_connections: if target_host in self._active_connections:
connection = Connection(self._active_connections[target_host]) connection = Connection(self._active_connections[target_host])
del self._active_connections[target_host] del self._active_connections[target_host]

@ -0,0 +1,23 @@
# (c) 2020 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.plugins.callback import CallbackBase
import os
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'callback_meta'
def __init__(self, *args, **kwargs):
super(CallbackModule, self).__init__(*args, **kwargs)
self.wants_implicit_tasks = os.environ.get('CB_WANTS_IMPLICIT', False)
def v2_playbook_on_task_start(self, task, is_conditional):
if task.implicit:
self._display.display('saw implicit task')
self._display.display(task.get_name())

@ -3,3 +3,6 @@
tasks: tasks:
- debug: - debug:
msg: "{{ username }}" msg: "{{ username }}"
- name: explicitly refresh inventory
meta: refresh_inventory

@ -36,6 +36,18 @@ env -u ANSIBLE_PLAYBOOK_DIR ANSIBLE_CONFIG=./playbookdir_cfg.ini ansible localho
# test adhoc callback triggers # test adhoc callback triggers
ANSIBLE_STDOUT_CALLBACK=callback_debug ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible --playbook-dir . testhost -i ../../inventory -m ping | grep -E '^v2_' | diff -u adhoc-callback.stdout - ANSIBLE_STDOUT_CALLBACK=callback_debug ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible --playbook-dir . testhost -i ../../inventory -m ping | grep -E '^v2_' | diff -u adhoc-callback.stdout -
# CB_WANTS_IMPLICIT isn't anything in Ansible itself.
# Our test cb plugin just accepts it. It lets us avoid copypasting the whole
# plugin just for two tests.
CB_WANTS_IMPLICIT=1 ANSIBLE_STDOUT_CALLBACK=callback_meta ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible-playbook -i ../../inventory --extra-vars @./vars.yml playbook.yml | grep 'saw implicit task'
set +e
if ANSIBLE_STDOUT_CALLBACK=callback_meta ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible-playbook -i ../../inventory --extra-vars @./vars.yml playbook.yml | grep 'saw implicit task'; then
echo "Callback got implicit task and should not have"
exit 1
fi
set -e
# Test that no tmp dirs are left behind when running ansible-config # Test that no tmp dirs are left behind when running ansible-config
TMP_DIR=~/.ansible/tmptest TMP_DIR=~/.ansible/tmptest
if [[ -d "$TMP_DIR" ]]; then if [[ -d "$TMP_DIR" ]]; then

Loading…
Cancel
Save