summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSloane Hertel <19572925+s-hertel@users.noreply.github.com>2022-06-08 13:07:21 -0400
committerGitHub <noreply@github.com>2022-06-08 12:07:21 -0500
commitdaf160bc67d740db406c956114f5fbe6bdb8c467 (patch)
tree241add88e8c2cd57611372dcf57a444a36e1fb6a
parentf4d7f33513c7735cb7cd71d490da32cf51a9dc4d (diff)
downloadansible-daf160bc67d740db406c956114f5fbe6bdb8c467.tar.gz
Add toggle to fix module_defaults with module-as-redirected-action on a per-module basis (#77265) (#77900)
* If there is a platform specific handler, prefer the resolved module over the resolved action when loading module_defaults Add a toggle for action plugins to prefer the resolved module when loading module_defaults Allow moving away from modules intercepted as actions pattern Fixes #77059 (cherry picked from commit 621e782ed0c119d2c84124d006fdf253c082449a)
-rw-r--r--changelogs/fragments/77265-module_defaults-with-modules-as-redirected-actions.yaml55
-rw-r--r--lib/ansible/executor/task_executor.py28
-rw-r--r--lib/ansible/playbook/base.py10
-rw-r--r--lib/ansible/plugins/loader.py19
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml46
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py18
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py18
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py18
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py35
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py35
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py35
-rw-r--r--test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py35
-rwxr-xr-xtest/integration/targets/module_defaults/runme.sh5
-rw-r--r--test/integration/targets/module_defaults/test_defaults.yml137
-rw-r--r--test/units/executor/test_task_executor.py21
15 files changed, 499 insertions, 16 deletions
diff --git a/changelogs/fragments/77265-module_defaults-with-modules-as-redirected-actions.yaml b/changelogs/fragments/77265-module_defaults-with-modules-as-redirected-actions.yaml
new file mode 100644
index 0000000000..a103f7c995
--- /dev/null
+++ b/changelogs/fragments/77265-module_defaults-with-modules-as-redirected-actions.yaml
@@ -0,0 +1,55 @@
+minor_changes:
+- |
+ Add an 'action_plugin' field for modules in runtime.yml plugin_routing.
+
+ This fixes module_defaults by supporting modules-as-redirected-actions
+ without redirecting module_defaults entries to the common action.
+
+ .. code: yaml
+
+ plugin_routing:
+ action:
+ facts:
+ redirect: ns.coll.eos
+ command:
+ redirect: ns.coll.eos
+ modules:
+ facts:
+ redirect: ns.coll.eos_facts
+ command:
+ redirect: ns.coll.eos_command
+
+ With the runtime.yml above for ns.coll, a task such as
+
+ .. code: yaml
+
+ - hosts: all
+ module_defaults:
+ ns.coll.eos_facts: {'valid_for_eos_facts': 'value'}
+ ns.coll.eos_command: {'not_valid_for_eos_facts': 'value'}
+ tasks:
+ - ns.coll.facts:
+
+ will end up with defaults for eos_facts and eos_command
+ since both modules redirect to the same action.
+
+ To select an action plugin for a module without merging
+ module_defaults, define an action_plugin field for the resolved
+ module in the runtime.yml.
+
+ .. code: yaml
+
+ plugin_routing:
+ modules:
+ facts:
+ redirect: ns.coll.eos_facts
+ action_plugin: ns.coll.eos
+ command:
+ redirect: ns.coll.eos_command
+ action_plugin: ns.coll.eos
+
+ The action_plugin field can be a redirected action plugin, as
+ it is resolved normally.
+
+ Using the modified runtime.yml, the example task will only use
+ the ns.coll.eos_facts defaults.
diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py
index 34e04afb9e..9b0ef23760 100644
--- a/lib/ansible/executor/task_executor.py
+++ b/lib/ansible/executor/task_executor.py
@@ -27,7 +27,7 @@ from ansible.playbook.conditional import Conditional
from ansible.playbook.task import Task
from ansible.plugins.loader import become_loader, cliconf_loader, connection_loader, httpapi_loader, netconf_loader, terminal_loader
from ansible.template import Templar
-from ansible.utils.collection_loader import AnsibleCollectionConfig
+from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef
from ansible.utils.listify import listify_lookup_plugin_terms
from ansible.utils.unsafe_proxy import to_unsafe_text, wrap_var
from ansible.vars.clean import namespace_facts, clean_facts
@@ -556,11 +556,16 @@ class TaskExecutor:
cvars['ansible_python_interpreter'] = sys.executable
# get handler
- self._handler = self._get_action_handler(connection=self._connection, templar=templar)
+ self._handler, module_context = self._get_action_handler_with_module_context(connection=self._connection, templar=templar)
+
+ if module_context is not None:
+ module_defaults_fqcn = module_context.resolved_fqcn
+ else:
+ module_defaults_fqcn = self._task.resolved_action
# Apply default params for action/module, if present
self._task.args = get_action_args_with_defaults(
- self._task.resolved_action, self._task.args, self._task.module_defaults, templar,
+ module_defaults_fqcn, self._task.args, self._task.module_defaults, templar,
action_groups=self._task._parent._play._action_groups
)
@@ -1069,7 +1074,12 @@ class TaskExecutor:
'''
Returns the correct action plugin to handle the requestion task action
'''
+ return self._get_action_handler_with_module_context(connection, templar)[0]
+ def _get_action_handler_with_module_context(self, connection, templar):
+ '''
+ Returns the correct action plugin to handle the requestion task action and the module context
+ '''
module_collection, separator, module_name = self._task.action.rpartition(".")
module_prefix = module_name.split('_')[0]
if module_collection:
@@ -1082,8 +1092,16 @@ class TaskExecutor:
collections = self._task.collections
+ # Check if the module has specified an action handler
+ module = self._shared_loader_obj.module_loader.find_plugin_with_context(
+ self._task.action, collection_list=collections
+ )
+ if not module.resolved or not module.action_plugin:
+ module = None
+ if module is not None:
+ handler_name = module.action_plugin
# let action plugin override module, fallback to 'normal' action plugin otherwise
- if self._shared_loader_obj.action_loader.has_plugin(self._task.action, collection_list=collections):
+ elif self._shared_loader_obj.action_loader.has_plugin(self._task.action, collection_list=collections):
handler_name = self._task.action
elif all((module_prefix in C.NETWORK_GROUP_MODULES, self._shared_loader_obj.action_loader.has_plugin(network_action, collection_list=collections))):
handler_name = network_action
@@ -1109,7 +1127,7 @@ class TaskExecutor:
if not handler:
raise AnsibleError("the handler '%s' was not found" % handler_name)
- return handler
+ return handler, module
def start_connection(play_context, variables, task_uuid):
diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py
index 564ce0cd3a..6150fc761f 100644
--- a/lib/ansible/playbook/base.py
+++ b/lib/ansible/playbook/base.py
@@ -505,9 +505,13 @@ class FieldAttributeBase(with_metaclass(BaseMeta, object)):
return fq_group_name, resolved_actions
def _resolve_action(self, action_name, mandatory=True):
- context = action_loader.find_plugin_with_context(action_name)
- if not context.resolved:
- context = module_loader.find_plugin_with_context(action_name)
+ context = module_loader.find_plugin_with_context(action_name)
+ if context.resolved and not context.action_plugin:
+ prefer = action_loader.find_plugin_with_context(action_name)
+ if prefer.resolved:
+ context = prefer
+ elif not context.resolved:
+ context = action_loader.find_plugin_with_context(action_name)
if context.resolved:
return context.resolved_fqcn
diff --git a/lib/ansible/plugins/loader.py b/lib/ansible/plugins/loader.py
index 0fbaff31dd..25eee2f398 100644
--- a/lib/ansible/plugins/loader.py
+++ b/lib/ansible/plugins/loader.py
@@ -130,6 +130,7 @@ class PluginLoadContext(object):
self.deprecation_warnings = []
self.resolved = False
self._resolved_fqcn = None
+ self.action_plugin = None
@property
def resolved_fqcn(self):
@@ -170,13 +171,14 @@ class PluginLoadContext(object):
self.deprecation_warnings.append(warning_text)
return self
- def resolve(self, resolved_name, resolved_path, resolved_collection, exit_reason):
+ def resolve(self, resolved_name, resolved_path, resolved_collection, exit_reason, action_plugin):
self.pending_redirect = None
self.plugin_resolved_name = resolved_name
self.plugin_resolved_path = resolved_path
self.plugin_resolved_collection = resolved_collection
self.exit_reason = exit_reason
self.resolved = True
+ self.action_plugin = action_plugin
return self
def redirect(self, redirect_name):
@@ -235,8 +237,12 @@ class PluginLoader:
self._searched_paths = set()
+ @property
+ def type(self):
+ return AnsibleCollectionRef.legacy_plugin_dir_to_plugin_type(self.subdir)
+
def __repr__(self):
- return 'PluginLoader(type={0})'.format(AnsibleCollectionRef.legacy_plugin_dir_to_plugin_type(self.subdir))
+ return 'PluginLoader(type={0})'.format(self.type)
def _clear_caches(self):
@@ -463,6 +469,7 @@ class PluginLoader:
# check collection metadata to see if any special handling is required for this plugin
routing_metadata = self._query_collection_routing_meta(acr, plugin_type, extension=extension)
+ action_plugin = None
# TODO: factor this into a wrapper method
if routing_metadata:
deprecation = routing_metadata.get('deprecation', None)
@@ -500,6 +507,9 @@ class PluginLoader:
return plugin_load_context.redirect(redirect)
# TODO: non-FQCN case, do we support `.` prefix for current collection, assume it with no dots, require it for subdirs in current, or ?
+ if self.type == 'modules':
+ action_plugin = routing_metadata.get('action_plugin')
+
n_resource = to_native(acr.resource, errors='strict')
# we want this before the extension is added
full_name = '{0}.{1}'.format(acr.n_python_package_name, n_resource)
@@ -522,7 +532,7 @@ class PluginLoader:
# FIXME: and is file or file link or ...
if os.path.exists(n_resource_path):
return plugin_load_context.resolve(
- full_name, to_text(n_resource_path), acr.collection, 'found exact match for {0} in {1}'.format(full_name, acr.collection))
+ full_name, to_text(n_resource_path), acr.collection, 'found exact match for {0} in {1}'.format(full_name, acr.collection), action_plugin)
if extension:
# the request was extension-specific, don't try for an extensionless match
@@ -541,7 +551,8 @@ class PluginLoader:
pass
return plugin_load_context.resolve(
- full_name, to_text(found_files[0]), acr.collection, 'found fuzzy extension match for {0} in {1}'.format(full_name, acr.collection))
+ full_name, to_text(found_files[0]), acr.collection,
+ 'found fuzzy extension match for {0} in {1}'.format(full_name, acr.collection), action_plugin)
def find_plugin(self, name, mod_type='', ignore_deprecated=False, check_aliases=False, collection_list=None):
''' Find a plugin named name '''
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml
index 081ee8c21d..a8c2c8c5e0 100644
--- a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml
@@ -1,3 +1,49 @@
+plugin_routing:
+ action:
+ # Backwards compat for modules-redirected-as-actions:
+ # By default, each module_defaults entry is resolved as an action plugin,
+ # and if it does not exist, it is resolved a a module.
+ # All modules that redirect to the same action will resolve to the same action.
+ module_uses_action_defaults:
+ redirect: testns.testcoll.eos
+
+ # module-redirected-as-action overridden by action_plugin
+ iosfacts:
+ redirect: testns.testcoll.nope
+ ios_facts:
+ redirect: testns.testcoll.nope
+
+ redirected_action:
+ redirect: testns.testcoll.ios
+ modules:
+ # Any module_defaults for testns.testcoll.module will not apply to a module_uses_action_defaults task:
+ #
+ # module_defaults:
+ # testns.testcoll.module:
+ # option: value
+ #
+ # But defaults for testns.testcoll.module_uses_action_defaults or testns.testcoll.eos will:
+ #
+ # module_defaults:
+ # testns.testcoll.module_uses_action_defaults:
+ # option: value
+ # testns.testcoll.eos:
+ # option: defined_last_i_win
+ module_uses_action_defaults:
+ redirect: testns.testcoll.module
+
+ # Not "eos_facts" to ensure TE is not finding handler via prefix
+ # eosfacts tasks should not get eos module_defaults (or defaults for other modules that use eos action plugin)
+ eosfacts:
+ action_plugin: testns.testcoll.eos
+
+ # Test that `action_plugin` has higher precedence than module-redirected-as-action - reverse this?
+ # Current behavior is iosfacts/ios_facts do not get ios defaults.
+ iosfacts:
+ redirect: testns.testcoll.ios_facts
+ ios_facts:
+ action_plugin: testns.testcoll.redirected_action
+
action_groups:
testgroup:
# Test metadata 'extend_group' feature does not get stuck in a recursive loop
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py
new file mode 100644
index 0000000000..0d39f26d4d
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py
@@ -0,0 +1,18 @@
+# Copyright: (c) 2022, 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.action.normal import ActionModule as ActionBase
+from ansible.utils.vars import merge_hash
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result['action_plugin'] = 'eos'
+
+ return result
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py
new file mode 100644
index 0000000000..20284fd1c0
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py
@@ -0,0 +1,18 @@
+# Copyright: (c) 2022, 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.action.normal import ActionModule as ActionBase
+from ansible.utils.vars import merge_hash
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result['action_plugin'] = 'ios'
+
+ return result
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py
new file mode 100644
index 0000000000..b0e1904b0d
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py
@@ -0,0 +1,18 @@
+# Copyright: (c) 2022, 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.action.normal import ActionModule as ActionBase
+from ansible.utils.vars import merge_hash
+
+
+class ActionModule(ActionBase):
+
+ def run(self, tmp=None, task_vars=None):
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ result['action_plugin'] = 'vyos'
+
+ return result
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py
new file mode 100644
index 0000000000..8c73fe15a1
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, 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 = r'''
+---
+module: eosfacts
+short_description: module to test module_defaults
+description: module to test module_defaults
+version_added: '2.13'
+'''
+
+EXAMPLES = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ eosfacts=dict(type=bool),
+ ),
+ supports_check_mode=True
+ )
+ module.exit_json(eosfacts=module.params['eosfacts'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py
new file mode 100644
index 0000000000..e2ed598164
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, 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 = r'''
+---
+module: ios_facts
+short_description: module to test module_defaults
+description: module to test module_defaults
+version_added: '2.13'
+'''
+
+EXAMPLES = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ ios_facts=dict(type=bool),
+ ),
+ supports_check_mode=True
+ )
+ module.exit_json(ios_facts=module.params['ios_facts'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py
new file mode 100644
index 0000000000..b98a5f9426
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, 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 = r'''
+---
+module: module
+short_description: module to test module_defaults
+description: module to test module_defaults
+version_added: '2.13'
+'''
+
+EXAMPLES = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ action_option=dict(type=bool),
+ ),
+ supports_check_mode=True
+ )
+ module.exit_json(action_option=module.params['action_option'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py
new file mode 100644
index 0000000000..3a9abbc66c
--- /dev/null
+++ b/test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, 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 = r'''
+---
+module: vyosfacts
+short_description: module to test module_defaults
+description: module to test module_defaults
+version_added: '2.13'
+'''
+
+EXAMPLES = r'''
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ vyosfacts=dict(type=bool),
+ ),
+ supports_check_mode=True
+ )
+ module.exit_json(vyosfacts=module.params['vyosfacts'])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/module_defaults/runme.sh b/test/integration/targets/module_defaults/runme.sh
index 082f4e5b1b..fe9c40ce62 100755
--- a/test/integration/targets/module_defaults/runme.sh
+++ b/test/integration/targets/module_defaults/runme.sh
@@ -2,8 +2,13 @@
set -eux
+# Symlink is test for backwards-compat (only workaround for https://github.com/ansible/ansible/issues/77059)
+sudo ln -s "${PWD}/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py" ./collections/ansible_collections/testns/testcoll/plugins/action/vyosfacts.py
+
ansible-playbook test_defaults.yml "$@"
+sudo rm ./collections/ansible_collections/testns/testcoll/plugins/action/vyosfacts.py
+
ansible-playbook test_action_groups.yml "$@"
ansible-playbook test_action_group_metadata.yml "$@"
diff --git a/test/integration/targets/module_defaults/test_defaults.yml b/test/integration/targets/module_defaults/test_defaults.yml
index 70377f12ce..6206d3a945 100644
--- a/test/integration/targets/module_defaults/test_defaults.yml
+++ b/test/integration/targets/module_defaults/test_defaults.yml
@@ -110,3 +110,140 @@
- "builtin_legacy_defaults_2.msg == 'legacy default'"
- include_tasks: tasks/main.yml
+
+- name: test preferring module name defaults for platform-specific actions
+ hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: ensure eosfacts does not use action plugin default
+ testns.testcoll.eosfacts:
+ module_defaults:
+ testns.testcoll.eos:
+ fail: true
+
+ - name: eosfacts does use module name defaults
+ testns.testcoll.eosfacts:
+ module_defaults:
+ testns.testcoll.eosfacts:
+ eosfacts: true
+ register: result
+
+ - assert:
+ that:
+ - result.eosfacts
+ - result.action_plugin == 'eos'
+
+ - name: ensure vyosfacts does not use action plugin default
+ testns.testcoll.vyosfacts:
+ module_defaults:
+ testns.testcoll.vyos:
+ fail: true
+
+ - name: vyosfacts does use vyosfacts defaults
+ testns.testcoll.vyosfacts:
+ module_defaults:
+ testns.testcoll.vyosfacts:
+ vyosfacts: true
+ register: result
+
+ - assert:
+ that:
+ - result.vyosfacts
+ - result.action_plugin == 'vyos'
+
+ - name: iosfacts/ios_facts does not use action plugin default (module action_plugin field has precedence over module-as-action-redirect)
+ collections:
+ - testns.testcoll
+ module_defaults:
+ testns.testcoll.ios:
+ fail: true
+ block:
+ - ios_facts:
+ register: result
+ - assert:
+ that:
+ - result.action_plugin == 'ios'
+
+ - iosfacts:
+ register: result
+ - assert:
+ that:
+ - result.action_plugin == 'ios'
+
+ - name: ensure iosfacts/ios_facts uses ios_facts defaults
+ collections:
+ - testns.testcoll
+ module_defaults:
+ testns.testcoll.ios_facts:
+ ios_facts: true
+ block:
+ - ios_facts:
+ register: result
+ - assert:
+ that:
+ - result.ios_facts
+ - result.action_plugin == 'ios'
+
+ - iosfacts:
+ register: result
+ - assert:
+ that:
+ - result.ios_facts
+ - result.action_plugin == 'ios'
+
+ - name: ensure iosfacts/ios_facts uses iosfacts defaults
+ collections:
+ - testns.testcoll
+ module_defaults:
+ testns.testcoll.iosfacts:
+ ios_facts: true
+ block:
+ - ios_facts:
+ register: result
+ - assert:
+ that:
+ - result.ios_facts
+ - result.action_plugin == 'ios'
+
+ - iosfacts:
+ register: result
+ - assert:
+ that:
+ - result.ios_facts
+ - result.action_plugin == 'ios'
+
+ - name: ensure redirected action gets redirected action defaults
+ testns.testcoll.module_uses_action_defaults:
+ module_defaults:
+ testns.testcoll.module_uses_action_defaults:
+ action_option: true
+ register: result
+
+ - assert:
+ that:
+ - result.action_option
+ - result.action_plugin == 'eos'
+
+ - name: ensure redirected action gets resolved action defaults
+ testns.testcoll.module_uses_action_defaults:
+ module_defaults:
+ testns.testcoll.eos:
+ action_option: true
+ register: result
+
+ - assert:
+ that:
+ - result.action_option
+ - result.action_plugin == 'eos'
+
+ - name: ensure redirected action does not use module-specific defaults
+ testns.testcoll.module_uses_action_defaults:
+ module_defaults:
+ testns.testcoll.module:
+ fail: true
+ register: result
+
+ - assert:
+ that:
+ - not result.action_option
+ - result.action_plugin == 'eos'
diff --git a/test/units/executor/test_task_executor.py b/test/units/executor/test_task_executor.py
index 8c01b339c1..7352774a4a 100644
--- a/test/units/executor/test_task_executor.py
+++ b/test/units/executor/test_task_executor.py
@@ -25,14 +25,18 @@ from units.compat import unittest
from units.compat.mock import patch, MagicMock
from ansible.errors import AnsibleError
from ansible.executor.task_executor import TaskExecutor, remove_omit
-from ansible.plugins.loader import action_loader, lookup_loader
+from ansible.plugins.loader import action_loader, lookup_loader, module_loader
from ansible.parsing.yaml.objects import AnsibleUnicode
from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
from ansible.module_utils.six import text_type
+from collections import namedtuple
from units.mock.loader import DictDataLoader
+get_with_context_result = namedtuple('get_with_context_result', ['object', 'plugin_load_context'])
+
+
class TestTaskExecutor(unittest.TestCase):
def test_task_executor_init(self):
@@ -204,6 +208,8 @@ class TestTaskExecutor(unittest.TestCase):
final_q=MagicMock(),
)
+ context = MagicMock(resolved=False)
+ te._shared_loader_obj.module_loader.find_plugin_with_context.return_value = context
action_loader = te._shared_loader_obj.action_loader
action_loader.has_plugin.return_value = True
action_loader.get.return_value = mock.sentinel.handler
@@ -238,6 +244,8 @@ class TestTaskExecutor(unittest.TestCase):
final_q=MagicMock(),
)
+ context = MagicMock(resolved=False)
+ te._shared_loader_obj.module_loader.find_plugin_with_context.return_value = context
action_loader = te._shared_loader_obj.action_loader
action_loader.has_plugin.side_effect = [False, True]
action_loader.get.return_value = mock.sentinel.handler
@@ -252,7 +260,7 @@ class TestTaskExecutor(unittest.TestCase):
handler = te._get_action_handler(mock_connection, mock_templar)
self.assertIs(mock.sentinel.handler, handler)
- action_loader.has_plugin.assert_has_calls([mock.call(action, collection_list=te._task.collections),
+ action_loader.has_plugin.assert_has_calls([mock.call(action, collection_list=te._task.collections), # called twice
mock.call(module_prefix, collection_list=te._task.collections)])
action_loader.get.assert_called_once_with(
@@ -277,6 +285,9 @@ class TestTaskExecutor(unittest.TestCase):
action_loader.has_plugin.return_value = False
action_loader.get.return_value = mock.sentinel.handler
action_loader.__contains__.return_value = False
+ module_loader = te._shared_loader_obj.module_loader
+ context = MagicMock(resolved=False)
+ module_loader.find_plugin_with_context.return_value = context
mock_connection = MagicMock()
mock_templar = MagicMock()
@@ -302,6 +313,7 @@ class TestTaskExecutor(unittest.TestCase):
mock_host = MagicMock()
mock_task = MagicMock()
+ mock_task.action = 'mock.action'
mock_task.args = dict()
mock_task.retries = 0
mock_task.delay = -1
@@ -328,7 +340,7 @@ class TestTaskExecutor(unittest.TestCase):
mock_action = MagicMock()
mock_queue = MagicMock()
- shared_loader = None
+ shared_loader = MagicMock()
new_stdin = None
job_vars = dict(omit="XXXXXXXXXXXXXXXXXXX")
@@ -344,7 +356,8 @@ class TestTaskExecutor(unittest.TestCase):
)
te._get_connection = MagicMock(return_value=mock_connection)
- te._get_action_handler = MagicMock(return_value=mock_action)
+ context = MagicMock()
+ te._get_action_handler_with_context = MagicMock(return_value=get_with_context_result(mock_action, context))
mock_action.run.return_value = dict(ansible_facts=dict())
res = te._execute()