Add a resolved_action task attribute (#74709)

* The resolved_action is the formatted version of the final plugin in the PluginLoadContext's redirect_list

* Collection plugins are represented as FQCN

* Legacy plugins are represented with only the plugin name

* Add tests

* Changelog
pull/75078/head
Sloane Hertel 3 years ago committed by GitHub
parent a4021977ad
commit 865bda3a11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
minor_changes:
- Task - Add a resolved_action attribute for Task objects to get the final resolved plugin.

@ -26,6 +26,7 @@ from ansible.module_utils._text import to_text
from ansible.parsing.splitter import parse_kv, split_args
from ansible.plugins.loader import module_loader, action_loader
from ansible.template import Templar
from ansible.utils.collection_loader import AnsibleCollectionRef
from ansible.utils.fqcn import add_internal_fqcns
from ansible.utils.sentinel import Sentinel
@ -120,6 +121,7 @@ class ModuleArgsParser:
self._task_attrs.update(['local_action', 'static'])
self._task_attrs = frozenset(self._task_attrs)
self.resolved_action = None
self.internal_redirect_list = []
def _split_module_string(self, module_string):
@ -299,6 +301,7 @@ class ModuleArgsParser:
# walk the filtered input dictionary to see if we recognize a module name
for item, value in iteritems(non_task_ds):
context = None
is_action_candidate = False
if item in BUILTIN_TASKS:
is_action_candidate = True
@ -306,6 +309,7 @@ class ModuleArgsParser:
is_action_candidate = True
else:
# If the plugin is resolved and redirected smuggle the list of candidate names via the task attribute 'internal_redirect_list'
# TODO: remove self.internal_redirect_list (and Task._ansible_internal_redirect_list) once TE can use the resolved name for module_defaults
context = action_loader.find_plugin_with_context(item, collection_list=self._collection_list)
if not context.resolved:
context = module_loader.find_plugin_with_context(item, collection_list=self._collection_list)
@ -314,12 +318,16 @@ class ModuleArgsParser:
elif context.redirect_list:
self.internal_redirect_list = context.redirect_list
is_action_candidate = bool(self.internal_redirect_list)
is_action_candidate = context.resolved and bool(context.redirect_list)
if is_action_candidate:
# finding more than one module name is a problem
if action is not None:
raise AnsibleParserError("conflicting action statements: %s, %s" % (action, item), obj=self._task_ds)
if context is not None and context.resolved:
self.resolved_action = context.resolved_fqcn
action = item
thing = value
action, args = self._normalize_parameters(thing, action=action, additional_args=additional_args)

@ -98,6 +98,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
self._role = role
self._parent = None
self.implicit = False
self.resolved_action = None
if task_include:
self._parent = task_include
@ -227,6 +228,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
raise AnsibleParserError(to_native(e), obj=ds, orig_exc=e)
else:
self._ansible_internal_redirect_list = args_parser.internal_redirect_list[:]
self.resolved_action = args_parser.resolved_action
# the command/shell/script modules used to support the `cmd` arg,
# which corresponds to what we now call _raw_params, so move that
@ -403,6 +405,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
new_me._role = self._role
new_me.implicit = self.implicit
new_me.resolved_action = self.resolved_action
return new_me
@ -421,6 +424,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
data['_ansible_internal_redirect_list'] = self._ansible_internal_redirect_list[:]
data['implicit'] = self.implicit
data['resolved_action'] = self.resolved_action
return data
@ -453,6 +457,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
self._ansible_internal_redirect_list = data.get('_ansible_internal_redirect_list', [])
self.implicit = data.get('implicit', False)
self.resolved_action = data.get('resolved_action')
super(Task, self).deserialize(data)

@ -129,6 +129,22 @@ class PluginLoadContext(object):
self.removal_version = None
self.deprecation_warnings = []
self.resolved = False
self._resolved_fqcn = None
@property
def resolved_fqcn(self):
if not self.resolved:
return
if not self._resolved_fqcn:
final_plugin = self.redirect_list[-1]
if AnsibleCollectionRef.is_valid_fqcr(final_plugin) and final_plugin.startswith('ansible.legacy.'):
final_plugin = final_plugin.split('ansible.legacy.')[-1]
if self.plugin_resolved_collection and not AnsibleCollectionRef.is_valid_fqcr(final_plugin):
final_plugin = self.plugin_resolved_collection + '.' + final_plugin
self._resolved_fqcn = final_plugin
return self._resolved_fqcn
def record_deprecation(self, name, deprecation, collection_name):
if not deprecation:

@ -137,3 +137,5 @@ if [[ "$(grep -wc "dynamic_host_a" "$CACHEFILE")" -ne "0" ]]; then
fi
./vars_plugin_tests.sh
./test_task_resolved_plugin.sh

@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -eux
export ANSIBLE_CALLBACKS_ENABLED=display_resolved_action
ansible-playbook test_task_resolved_plugin/unqualified.yml "$@" | tee out.txt
action_resolution=(
"legacy_action == legacy_action"
"legacy_module == legacy_module"
"debug == ansible.builtin.debug"
"ping == ansible.builtin.ping"
)
for result in "${action_resolution[@]}"; do
grep -q out.txt -e "$result"
done
ansible-playbook test_task_resolved_plugin/unqualified_and_collections_kw.yml "$@" | tee out.txt
action_resolution=(
"legacy_action == legacy_action"
"legacy_module == legacy_module"
"debug == ansible.builtin.debug"
"ping == ansible.builtin.ping"
"collection_action == test_ns.test_coll.collection_action"
"collection_module == test_ns.test_coll.collection_module"
"formerly_action == test_ns.test_coll.collection_action"
"formerly_module == test_ns.test_coll.collection_module"
)
for result in "${action_resolution[@]}"; do
grep -q out.txt -e "$result"
done
ansible-playbook test_task_resolved_plugin/fqcn.yml "$@" | tee out.txt
action_resolution=(
"ansible.legacy.legacy_action == legacy_action"
"ansible.legacy.legacy_module == legacy_module"
"ansible.legacy.debug == ansible.builtin.debug"
"ansible.legacy.ping == ansible.builtin.ping"
"ansible.builtin.debug == ansible.builtin.debug"
"ansible.builtin.ping == ansible.builtin.ping"
"test_ns.test_coll.collection_action == test_ns.test_coll.collection_action"
"test_ns.test_coll.collection_module == test_ns.test_coll.collection_module"
"test_ns.test_coll.formerly_action == test_ns.test_coll.collection_action"
"test_ns.test_coll.formerly_module == test_ns.test_coll.collection_module"
)
for result in "${action_resolution[@]}"; do
grep -q out.txt -e "$result"
done

@ -0,0 +1,14 @@
# 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.action import ActionBase
class ActionModule(ActionBase):
TRANSFERS_FILES = False
_VALID_ARGS = frozenset()
def run(self, tmp=None, task_vars=None):
return {'changed': False}

@ -0,0 +1,37 @@
# (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
DOCUMENTATION = '''
name: display_resolved_action
type: aggregate
short_description: Displays the requested and resolved actions at the end of a playbook.
description:
- Displays the requested and resolved actions in the format "requested == resolved".
requirements:
- Enable in configuration.
'''
from ansible import constants as C
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'display_resolved_action'
CALLBACK_NEEDS_ENABLED = True
def __init__(self, *args, **kwargs):
super(CallbackModule, self).__init__(*args, **kwargs)
self.requested_to_resolved = {}
def v2_playbook_on_task_start(self, task, is_conditional):
self.requested_to_resolved[task.action] = task.resolved_action
def v2_playbook_on_stats(self, stats):
for requested, resolved in self.requested_to_resolved.items():
self._display.display("%s == %s" % (requested, resolved), screen_only=True)

@ -0,0 +1,7 @@
plugin_routing:
modules:
formerly_module:
redirect: test_ns.test_coll.collection_module
action:
formerly_action:
redirect: test_ns.test_coll.collection_action

@ -0,0 +1,14 @@
# 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.action import ActionBase
class ActionModule(ActionBase):
TRANSFERS_FILES = False
_VALID_ARGS = frozenset()
def run(self, tmp=None, task_vars=None):
return {'changed': False}

@ -0,0 +1,29 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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
DOCUMENTATION = '''
---
module: collection_module
short_description: A module to test a task's resolved action name.
description: A module to test a task's resolved action name.
options: {}
author: Ansible Core Team
notes:
- Supports C(check_mode).
'''
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(supports_check_mode=True, argument_spec={})
module.exit_json(changed=False)
if __name__ == '__main__':
main()

@ -0,0 +1,14 @@
---
- hosts: localhost
gather_facts: no
tasks:
- ansible.legacy.legacy_action:
- ansible.legacy.legacy_module:
- ansible.legacy.debug:
- ansible.legacy.ping:
- ansible.builtin.debug:
- ansible.builtin.ping:
- test_ns.test_coll.collection_action:
- test_ns.test_coll.collection_module:
- test_ns.test_coll.formerly_action:
- test_ns.test_coll.formerly_module:

@ -0,0 +1,29 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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
DOCUMENTATION = '''
---
module: legacy_module
short_description: A module to test a task's resolved action name.
description: A module to test a task's resolved action name.
options: {}
author: Ansible Core Team
notes:
- Supports C(check_mode).
'''
from ansible.module_utils.basic import AnsibleModule
def main():
module = AnsibleModule(supports_check_mode=True, argument_spec={})
module.exit_json(changed=False)
if __name__ == '__main__':
main()

@ -0,0 +1,8 @@
---
- hosts: localhost
gather_facts: no
tasks:
- legacy_action:
- legacy_module:
- debug:
- ping:

@ -0,0 +1,14 @@
---
- hosts: localhost
gather_facts: no
collections:
- test_ns.test_coll
tasks:
- legacy_action:
- legacy_module:
- debug:
- ping:
- collection_action:
- collection_module:
- formerly_action:
- formerly_module:
Loading…
Cancel
Save