1# Copyright (c) 2017 Ansible Project
2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
4from __future__ import (absolute_import, division, print_function)
5__metaclass__ = type
6
7import os
8import time
9
10from ansible import constants as C
11from ansible.executor.module_common import get_action_args_with_defaults
12from ansible.module_utils.parsing.convert_bool import boolean
13from ansible.plugins.action import ActionBase
14from ansible.utils.vars import merge_hash
15
16
17class ActionModule(ActionBase):
18
19    def _get_module_args(self, fact_module, task_vars):
20
21        mod_args = self._task.args.copy()
22
23        # deal with 'setup specific arguments'
24        if fact_module not in C._ACTION_SETUP:
25            # network facts modules must support gather_subset
26            if self._connection._load_name not in ('network_cli', 'httpapi', 'netconf'):
27                subset = mod_args.pop('gather_subset', None)
28                if subset not in ('all', ['all']):
29                    self._display.warning('Ignoring subset(%s) for %s' % (subset, fact_module))
30
31            timeout = mod_args.pop('gather_timeout', None)
32            if timeout is not None:
33                self._display.warning('Ignoring timeout(%s) for %s' % (timeout, fact_module))
34
35            fact_filter = mod_args.pop('filter', None)
36            if fact_filter is not None:
37                self._display.warning('Ignoring filter(%s) for %s' % (fact_filter, fact_module))
38
39        # Strip out keys with ``None`` values, effectively mimicking ``omit`` behavior
40        # This ensures we don't pass a ``None`` value as an argument expecting a specific type
41        mod_args = dict((k, v) for k, v in mod_args.items() if v is not None)
42
43        # handle module defaults
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        )
51
52        return mod_args
53
54    def _combine_task_result(self, result, task_result):
55        filtered_res = {
56            'ansible_facts': task_result.get('ansible_facts', {}),
57            'warnings': task_result.get('warnings', []),
58            'deprecations': task_result.get('deprecations', []),
59        }
60
61        # on conflict the last plugin processed wins, but try to do deep merge and append to lists.
62        return merge_hash(result, filtered_res, list_merge='append_rp')
63
64    def run(self, tmp=None, task_vars=None):
65
66        self._supports_check_mode = True
67
68        result = super(ActionModule, self).run(tmp, task_vars)
69        result['ansible_facts'] = {}
70
71        # copy the value with list() so we don't mutate the config
72        modules = list(C.config.get_config_value('FACTS_MODULES', variables=task_vars))
73
74        parallel = task_vars.pop('ansible_facts_parallel', self._task.args.pop('parallel', None))
75        if 'smart' in modules:
76            connection_map = C.config.get_config_value('CONNECTION_FACTS_MODULES', variables=task_vars)
77            network_os = self._task.args.get('network_os', task_vars.get('ansible_network_os', task_vars.get('ansible_facts', {}).get('network_os')))
78            modules.extend([connection_map.get(network_os or self._connection._load_name, 'ansible.legacy.setup')])
79            modules.pop(modules.index('smart'))
80
81        failed = {}
82        skipped = {}
83
84        if parallel is None and len(modules) >= 1:
85            parallel = True
86        else:
87            parallel = boolean(parallel)
88
89        if parallel:
90            # serially execute each module
91            for fact_module in modules:
92                # just one module, no need for fancy async
93                mod_args = self._get_module_args(fact_module, task_vars)
94                res = self._execute_module(module_name=fact_module, module_args=mod_args, task_vars=task_vars, wrap_async=False)
95                if res.get('failed', False):
96                    failed[fact_module] = res
97                elif res.get('skipped', False):
98                    skipped[fact_module] = res
99                else:
100                    result = self._combine_task_result(result, res)
101
102            self._remove_tmp_path(self._connection._shell.tmpdir)
103        else:
104            # do it async
105            jobs = {}
106            for fact_module in modules:
107                mod_args = self._get_module_args(fact_module, task_vars)
108                self._display.vvvv("Running %s" % fact_module)
109                jobs[fact_module] = (self._execute_module(module_name=fact_module, module_args=mod_args, task_vars=task_vars, wrap_async=True))
110
111            while jobs:
112                for module in jobs:
113                    poll_args = {'jid': jobs[module]['ansible_job_id'], '_async_dir': os.path.dirname(jobs[module]['results_file'])}
114                    res = self._execute_module(module_name='ansible.legacy.async_status', module_args=poll_args, task_vars=task_vars, wrap_async=False)
115                    if res.get('finished', 0) == 1:
116                        if res.get('failed', False):
117                            failed[module] = res
118                        elif res.get('skipped', False):
119                            skipped[module] = res
120                        else:
121                            result = self._combine_task_result(result, res)
122                        del jobs[module]
123                        break
124                    else:
125                        time.sleep(0.1)
126                else:
127                    time.sleep(0.5)
128
129        if skipped:
130            result['msg'] = "The following modules were skipped: %s\n" % (', '.join(skipped.keys()))
131            result['skipped_modules'] = skipped
132            if len(skipped) == len(modules):
133                result['skipped'] = True
134
135        if failed:
136            result['failed'] = True
137            result['msg'] = "The following modules failed to execute: %s\n" % (', '.join(failed.keys()))
138            result['failed_modules'] = failed
139
140        # tell executor facts were gathered
141        result['ansible_facts']['_ansible_facts_gathered'] = True
142
143        # hack to keep --verbose from showing all the setup module result
144        result['_ansible_verbose_override'] = True
145
146        return result
147