Browse code

[2.10] Fix using module-specific module_defaults in action plugins (#74850)

* 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)

Sloane Hertel authored on 2021/06/15 00:08:10
Showing 10 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,2 @@
0
+bugfixes:
1
+  - gather_facts, package, service - fix using module_defaults for the modules in addition to the action plugins. (https://github.com/ansible/ansible/issues/72918)
... ...
@@ -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'])