* Use the module redirect_list when getting defaults for action plugins (#73864)
* Fix module-specific defaults in the gather_facts, package, and service action plugins.
* Handle ansible.legacy actions better in get_action_args_with_defaults
* Add tests for each action plugin
* Changelog
Fixes #72918
(cherry picked from commit 5640093f1ca63fd6af231cc8a7fb7d40e1907b8c)
* Fix tests for < 3.8
(cherry picked from commit 267b7215b3140f1a02935916970ecf98e0aef0c8)
... | ... |
@@ -1420,9 +1420,14 @@ def get_action_args_with_defaults(action, args, defaults, templar, redirected_na |
1420 | 1420 |
tmp_args.update((module_defaults.get('group/%s' % group_name) or {}).copy()) |
1421 | 1421 |
|
1422 | 1422 |
# handle specific action defaults |
1423 |
- for action in redirected_names: |
|
1424 |
- if action in module_defaults: |
|
1425 |
- tmp_args.update(module_defaults[action].copy()) |
|
1423 |
+ for redirected_action in redirected_names: |
|
1424 |
+ legacy = None |
|
1425 |
+ if redirected_action.startswith('ansible.legacy.') and action == redirected_action: |
|
1426 |
+ legacy = redirected_action.split('ansible.legacy.')[-1] |
|
1427 |
+ if legacy and legacy in module_defaults: |
|
1428 |
+ tmp_args.update(module_defaults[legacy].copy()) |
|
1429 |
+ if redirected_action in module_defaults: |
|
1430 |
+ tmp_args.update(module_defaults[redirected_action].copy()) |
|
1426 | 1431 |
|
1427 | 1432 |
# direct args override all |
1428 | 1433 |
tmp_args.update(args) |
... | ... |
@@ -41,7 +41,13 @@ class ActionModule(ActionBase): |
41 | 41 |
mod_args = dict((k, v) for k, v in mod_args.items() if v is not None) |
42 | 42 |
|
43 | 43 |
# handle module defaults |
44 |
- mod_args = get_action_args_with_defaults(fact_module, mod_args, self._task.module_defaults, self._templar, self._task._ansible_internal_redirect_list) |
|
44 |
+ redirect_list = self._shared_loader_obj.module_loader.find_plugin_with_context( |
|
45 |
+ fact_module, collection_list=self._task.collections |
|
46 |
+ ).redirect_list |
|
47 |
+ |
|
48 |
+ mod_args = get_action_args_with_defaults( |
|
49 |
+ fact_module, mod_args, self._task.module_defaults, self._templar, redirect_list |
|
50 |
+ ) |
|
45 | 51 |
|
46 | 52 |
return mod_args |
47 | 53 |
|
... | ... |
@@ -62,7 +68,9 @@ class ActionModule(ActionBase): |
62 | 62 |
result = super(ActionModule, self).run(tmp, task_vars) |
63 | 63 |
result['ansible_facts'] = {} |
64 | 64 |
|
65 |
- modules = C.config.get_config_value('FACTS_MODULES', variables=task_vars) |
|
65 |
+ # copy the value with list() so we don't mutate the config |
|
66 |
+ modules = list(C.config.get_config_value('FACTS_MODULES', variables=task_vars)) |
|
67 |
+ |
|
66 | 68 |
parallel = task_vars.pop('ansible_facts_parallel', self._task.args.pop('parallel', None)) |
67 | 69 |
if 'smart' in modules: |
68 | 70 |
connection_map = C.config.get_config_value('CONNECTION_FACTS_MODULES', variables=task_vars) |
... | ... |
@@ -71,8 +71,9 @@ class ActionModule(ActionBase): |
71 | 71 |
del new_module_args['use'] |
72 | 72 |
|
73 | 73 |
# get defaults for specific module |
74 |
+ context = self._shared_loader_obj.module_loader.find_plugin_with_context(module, collection_list=self._task.collections) |
|
74 | 75 |
new_module_args = get_action_args_with_defaults( |
75 |
- module, new_module_args, self._task.module_defaults, self._templar, self._task._ansible_internal_redirect_list |
|
76 |
+ module, new_module_args, self._task.module_defaults, self._templar, context.redirect_list |
|
76 | 77 |
) |
77 | 78 |
|
78 | 79 |
if module in self.BUILTIN_PKG_MGR_MODULES: |
... | ... |
@@ -79,8 +79,9 @@ class ActionModule(ActionBase): |
79 | 79 |
self._display.warning('Ignoring "%s" as it is not used in "%s"' % (unused, module)) |
80 | 80 |
|
81 | 81 |
# get defaults for specific module |
82 |
+ context = self._shared_loader_obj.module_loader.find_plugin_with_context(module, collection_list=self._task.collections) |
|
82 | 83 |
new_module_args = get_action_args_with_defaults( |
83 |
- module, new_module_args, self._task.module_defaults, self._templar, self._task._ansible_internal_redirect_list |
|
84 |
+ module, new_module_args, self._task.module_defaults, self._templar, context.redirect_list |
|
84 | 85 |
) |
85 | 86 |
|
86 | 87 |
# collection prefix known internal modules to avoid collisions from collections search, while still allowing library/ overrides |
... | ... |
@@ -19,3 +19,7 @@ ansible-playbook prevent_clobbering.yml -v "$@" |
19 | 19 |
|
20 | 20 |
# ensure we dont fail module on bad subset |
21 | 21 |
ansible-playbook verify_subset.yml "$@" |
22 |
+ |
|
23 |
+# ensure we can set defaults for the action plugin and facts module |
|
24 |
+ansible-playbook test_module_defaults.yml "$@" --tags default_fact_module |
|
25 |
+ANSIBLE_FACTS_MODULES='ansible.legacy.setup' ansible-playbook test_module_defaults.yml "$@" --tags custom_fact_module |
22 | 26 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,79 @@ |
0 |
+--- |
|
1 |
+- hosts: localhost |
|
2 |
+ # The gather_facts keyword has default values for its |
|
3 |
+ # options so module_defaults doesn't have much affect. |
|
4 |
+ gather_facts: no |
|
5 |
+ tags: |
|
6 |
+ - default_fact_module |
|
7 |
+ tasks: |
|
8 |
+ - name: set defaults for the action plugin |
|
9 |
+ gather_facts: |
|
10 |
+ module_defaults: |
|
11 |
+ gather_facts: |
|
12 |
+ gather_subset: min |
|
13 |
+ |
|
14 |
+ - assert: |
|
15 |
+ that: "gather_subset == ['min']" |
|
16 |
+ |
|
17 |
+ - name: set defaults for the module |
|
18 |
+ gather_facts: |
|
19 |
+ module_defaults: |
|
20 |
+ setup: |
|
21 |
+ gather_subset: '!all' |
|
22 |
+ |
|
23 |
+ - assert: |
|
24 |
+ that: "gather_subset == ['!all']" |
|
25 |
+ |
|
26 |
+ # Defaults for the action plugin win because they are |
|
27 |
+ # loaded first and options need to be omitted for |
|
28 |
+ # defaults to be used. |
|
29 |
+ - name: set defaults for the action plugin and module |
|
30 |
+ gather_facts: |
|
31 |
+ module_defaults: |
|
32 |
+ setup: |
|
33 |
+ gather_subset: '!all' |
|
34 |
+ gather_facts: |
|
35 |
+ gather_subset: min |
|
36 |
+ |
|
37 |
+ - assert: |
|
38 |
+ that: "gather_subset == ['min']" |
|
39 |
+ |
|
40 |
+ # The gather_facts 'smart' facts module is 'ansible.legacy.setup' by default. |
|
41 |
+ # If 'setup' (the unqualified name) is explicitly requested, FQCN module_defaults |
|
42 |
+ # would not apply. |
|
43 |
+ - name: set defaults for the fqcn module |
|
44 |
+ gather_facts: |
|
45 |
+ module_defaults: |
|
46 |
+ ansible.legacy.setup: |
|
47 |
+ gather_subset: '!all' |
|
48 |
+ |
|
49 |
+ - assert: |
|
50 |
+ that: "gather_subset == ['!all']" |
|
51 |
+ |
|
52 |
+- hosts: localhost |
|
53 |
+ gather_facts: no |
|
54 |
+ tags: |
|
55 |
+ - custom_fact_module |
|
56 |
+ tasks: |
|
57 |
+ - name: set defaults for the module |
|
58 |
+ gather_facts: |
|
59 |
+ module_defaults: |
|
60 |
+ ansible.legacy.setup: |
|
61 |
+ gather_subset: '!all' |
|
62 |
+ |
|
63 |
+ - assert: |
|
64 |
+ that: |
|
65 |
+ - "gather_subset == ['!all']" |
|
66 |
+ |
|
67 |
+ # Defaults for the action plugin win. |
|
68 |
+ - name: set defaults for the action plugin and module |
|
69 |
+ gather_facts: |
|
70 |
+ module_defaults: |
|
71 |
+ gather_facts: |
|
72 |
+ gather_subset: min |
|
73 |
+ ansible.legacy.setup: |
|
74 |
+ gather_subset: '!all' |
|
75 |
+ |
|
76 |
+ - assert: |
|
77 |
+ that: |
|
78 |
+ - "gather_subset == ['min']" |
... | ... |
@@ -74,6 +74,38 @@ |
74 | 74 |
## package |
75 | 75 |
## |
76 | 76 |
|
77 |
+# Verify module_defaults for package and the underlying module are utilized |
|
78 |
+# Validates: https://github.com/ansible/ansible/issues/72918 |
|
79 |
+- block: |
|
80 |
+ # 'name' is required |
|
81 |
+ - name: install apt with package defaults |
|
82 |
+ package: |
|
83 |
+ module_defaults: |
|
84 |
+ package: |
|
85 |
+ name: apt |
|
86 |
+ state: present |
|
87 |
+ |
|
88 |
+ - name: install apt with dnf defaults (auto) |
|
89 |
+ package: |
|
90 |
+ module_defaults: |
|
91 |
+ dnf: |
|
92 |
+ name: apt |
|
93 |
+ state: present |
|
94 |
+ |
|
95 |
+ - name: install apt with dnf defaults (use dnf) |
|
96 |
+ package: |
|
97 |
+ use: dnf |
|
98 |
+ module_defaults: |
|
99 |
+ dnf: |
|
100 |
+ name: apt |
|
101 |
+ state: present |
|
102 |
+ always: |
|
103 |
+ - name: remove apt |
|
104 |
+ dnf: |
|
105 |
+ name: apt |
|
106 |
+ state: absent |
|
107 |
+ when: ansible_distribution == "Fedora" |
|
108 |
+ |
|
77 | 109 |
- name: define distros to attempt installing at on |
78 | 110 |
set_fact: |
79 | 111 |
package_distros: |
... | ... |
@@ -11,6 +11,39 @@ |
11 | 11 |
that: |
12 | 12 |
- "enable_in_check_mode_result is changed" |
13 | 13 |
|
14 |
+- name: (check mode run) test that service defaults are used |
|
15 |
+ service: |
|
16 |
+ register: enable_in_check_mode_result |
|
17 |
+ check_mode: yes |
|
18 |
+ module_defaults: |
|
19 |
+ service: |
|
20 |
+ name: ansible_test |
|
21 |
+ enabled: yes |
|
22 |
+ |
|
23 |
+- name: assert that changes reported for check mode run |
|
24 |
+ assert: |
|
25 |
+ that: |
|
26 |
+ - "enable_in_check_mode_result is changed" |
|
27 |
+ |
|
28 |
+- name: (check mode run) test that specific module defaults are used |
|
29 |
+ service: |
|
30 |
+ register: enable_in_check_mode_result |
|
31 |
+ check_mode: yes |
|
32 |
+ when: "ansible_service_mgr in ['sysvinit', 'systemd']" |
|
33 |
+ module_defaults: |
|
34 |
+ sysvinit: |
|
35 |
+ name: ansible_test |
|
36 |
+ enabled: yes |
|
37 |
+ systemd: |
|
38 |
+ name: ansible_test |
|
39 |
+ enabled: yes |
|
40 |
+ |
|
41 |
+- name: assert that changes reported for check mode run |
|
42 |
+ assert: |
|
43 |
+ that: |
|
44 |
+ - "enable_in_check_mode_result is changed" |
|
45 |
+ when: "ansible_service_mgr in ['sysvinit', 'systemd']" |
|
46 |
+ |
|
14 | 47 |
- name: enable the ansible test service |
15 | 48 |
service: name=ansible_test enabled=yes |
16 | 49 |
register: enable_result |
... | ... |
@@ -22,8 +22,9 @@ from units.compat import unittest |
22 | 22 |
from units.compat.mock import MagicMock, patch |
23 | 23 |
|
24 | 24 |
from ansible import constants as C |
25 |
-from ansible.plugins.action.gather_facts import ActionModule |
|
26 | 25 |
from ansible.playbook.task import Task |
26 |
+from ansible.plugins.action.gather_facts import ActionModule as GatherFactsAction |
|
27 |
+from ansible.plugins import loader as plugin_loader |
|
27 | 28 |
from ansible.template import Templar |
28 | 29 |
import ansible.executor.module_common as module_common |
29 | 30 |
|
... | ... |
@@ -45,15 +46,64 @@ class TestNetworkFacts(unittest.TestCase): |
45 | 45 |
def tearDown(self): |
46 | 46 |
pass |
47 | 47 |
|
48 |
+ @patch.object(module_common, '_get_collection_metadata', return_value={}) |
|
49 |
+ def test_network_gather_facts_smart_facts_module(self, mock_collection_metadata): |
|
50 |
+ self.fqcn_task_vars = {'ansible_network_os': 'ios'} |
|
51 |
+ self.task.action = 'gather_facts' |
|
52 |
+ self.task.async_val = False |
|
53 |
+ self.task.args = {} |
|
54 |
+ |
|
55 |
+ plugin = GatherFactsAction(self.task, self.connection, self.play_context, loader=None, templar=self.templar, shared_loader_obj=None) |
|
56 |
+ get_module_args = MagicMock() |
|
57 |
+ plugin._get_module_args = get_module_args |
|
58 |
+ plugin._execute_module = MagicMock() |
|
59 |
+ |
|
60 |
+ res = plugin.run(task_vars=self.fqcn_task_vars) |
|
61 |
+ |
|
62 |
+ # assert the gather_facts config is 'smart' |
|
63 |
+ facts_modules = C.config.get_config_value('FACTS_MODULES', variables=self.fqcn_task_vars) |
|
64 |
+ self.assertEqual(facts_modules, ['smart']) |
|
65 |
+ |
|
66 |
+ # assert the correct module was found |
|
67 |
+ self.assertEqual(get_module_args.call_count, 1) |
|
68 |
+ |
|
69 |
+ get_module_args.assert_called_once_with( |
|
70 |
+ 'ansible.legacy.ios_facts', {'ansible_network_os': 'ios'}, |
|
71 |
+ ) |
|
72 |
+ |
|
73 |
+ @patch.object(module_common, '_get_collection_metadata', return_value={}) |
|
74 |
+ def test_network_gather_facts_smart_facts_module_fqcn(self, mock_collection_metadata): |
|
75 |
+ self.fqcn_task_vars = {'ansible_network_os': 'cisco.ios.ios'} |
|
76 |
+ self.task.action = 'gather_facts' |
|
77 |
+ self.task.async_val = False |
|
78 |
+ self.task.args = {} |
|
79 |
+ |
|
80 |
+ plugin = GatherFactsAction(self.task, self.connection, self.play_context, loader=None, templar=self.templar, shared_loader_obj=None) |
|
81 |
+ get_module_args = MagicMock() |
|
82 |
+ plugin._get_module_args = get_module_args |
|
83 |
+ plugin._execute_module = MagicMock() |
|
84 |
+ |
|
85 |
+ res = plugin.run(task_vars=self.fqcn_task_vars) |
|
86 |
+ |
|
87 |
+ # assert the gather_facts config is 'smart' |
|
88 |
+ facts_modules = C.config.get_config_value('FACTS_MODULES', variables=self.fqcn_task_vars) |
|
89 |
+ self.assertEqual(facts_modules, ['smart']) |
|
90 |
+ |
|
91 |
+ # assert the correct module was found |
|
92 |
+ self.assertEqual(get_module_args.call_count, 1) |
|
93 |
+ |
|
94 |
+ get_module_args.assert_called_once_with( |
|
95 |
+ 'cisco.ios.ios_facts', {'ansible_network_os': 'cisco.ios.ios'}, |
|
96 |
+ ) |
|
97 |
+ |
|
48 | 98 |
def test_network_gather_facts(self): |
49 | 99 |
self.task_vars = {'ansible_network_os': 'ios'} |
50 | 100 |
self.task.action = 'gather_facts' |
51 | 101 |
self.task.async_val = False |
52 |
- self.task._ansible_internal_redirect_list = [] |
|
53 | 102 |
self.task.args = {'gather_subset': 'min'} |
54 | 103 |
self.task.module_defaults = [{'ios_facts': {'gather_subset': 'min'}}] |
55 | 104 |
|
56 |
- plugin = ActionModule(self.task, self.connection, self.play_context, loader=None, templar=self.templar, shared_loader_obj=None) |
|
105 |
+ plugin = GatherFactsAction(self.task, self.connection, self.play_context, loader=None, templar=self.templar, shared_loader_obj=plugin_loader) |
|
57 | 106 |
plugin._execute_module = MagicMock() |
58 | 107 |
|
59 | 108 |
res = plugin.run(task_vars=self.task_vars) |
... | ... |
@@ -62,19 +112,15 @@ class TestNetworkFacts(unittest.TestCase): |
62 | 62 |
mod_args = plugin._get_module_args('ios_facts', task_vars=self.task_vars) |
63 | 63 |
self.assertEqual(mod_args['gather_subset'], 'min') |
64 | 64 |
|
65 |
- facts_modules = C.config.get_config_value('FACTS_MODULES', variables=self.task_vars) |
|
66 |
- self.assertEqual(facts_modules, ['ansible.legacy.ios_facts']) |
|
67 |
- |
|
68 | 65 |
@patch.object(module_common, '_get_collection_metadata', return_value={}) |
69 | 66 |
def test_network_gather_facts_fqcn(self, mock_collection_metadata): |
70 | 67 |
self.fqcn_task_vars = {'ansible_network_os': 'cisco.ios.ios'} |
71 | 68 |
self.task.action = 'gather_facts' |
72 |
- self.task._ansible_internal_redirect_list = ['cisco.ios.ios_facts'] |
|
73 | 69 |
self.task.async_val = False |
74 | 70 |
self.task.args = {'gather_subset': 'min'} |
75 | 71 |
self.task.module_defaults = [{'cisco.ios.ios_facts': {'gather_subset': 'min'}}] |
76 | 72 |
|
77 |
- plugin = ActionModule(self.task, self.connection, self.play_context, loader=None, templar=self.templar, shared_loader_obj=None) |
|
73 |
+ plugin = GatherFactsAction(self.task, self.connection, self.play_context, loader=None, templar=self.templar, shared_loader_obj=plugin_loader) |
|
78 | 74 |
plugin._execute_module = MagicMock() |
79 | 75 |
|
80 | 76 |
res = plugin.run(task_vars=self.fqcn_task_vars) |
... | ... |
@@ -82,6 +128,3 @@ class TestNetworkFacts(unittest.TestCase): |
82 | 82 |
|
83 | 83 |
mod_args = plugin._get_module_args('cisco.ios.ios_facts', task_vars=self.fqcn_task_vars) |
84 | 84 |
self.assertEqual(mod_args['gather_subset'], 'min') |
85 |
- |
|
86 |
- facts_modules = C.config.get_config_value('FACTS_MODULES', variables=self.fqcn_task_vars) |
|
87 |
- self.assertEqual(facts_modules, ['cisco.ios.ios_facts']) |