1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
|
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
import os
import time
import typing as t
from ansible import constants as C
from ansible.errors import AnsibleActionFail
from ansible.executor.module_common import _apply_action_arg_defaults
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
from ansible.utils.vars import merge_hash
from ansible._internal._errors import _error_utils
class ActionModule(ActionBase):
_supports_check_mode = True
def _get_module_args(self, fact_module: str, task_vars: dict[str, t.Any]) -> dict[str, t.Any]:
mod_args = self._task.args.copy()
# deal with 'setup specific arguments'
if fact_module not in C._ACTION_SETUP:
# TODO: remove in favor of controller side argspec detecting valid arguments
# network facts modules must support gather_subset
name = self._connection.ansible_name.removeprefix('ansible.netcommon.')
if name not in ('network_cli', 'httpapi', 'netconf'):
subset = mod_args.pop('gather_subset', None)
if subset not in ('all', ['all'], None):
self._display.warning('Not passing subset(%s) to %s' % (subset, fact_module))
timeout = mod_args.pop('gather_timeout', None)
if timeout is not None:
self._display.warning('Not passing timeout(%s) to %s' % (timeout, fact_module))
fact_filter = mod_args.pop('filter', None)
if fact_filter is not None:
self._display.warning('Not passing filter(%s) to %s' % (fact_filter, fact_module))
# Strip out keys with ``None`` values, effectively mimicking ``omit`` behavior
# This ensures we don't pass a ``None`` value as an argument expecting a specific type
mod_args = dict((k, v) for k, v in mod_args.items() if v is not None)
# handle module defaults
resolved_fact_module = self._shared_loader_obj.module_loader.find_plugin_with_context(
fact_module, collection_list=self._task.collections
).resolved_fqcn
mod_args = _apply_action_arg_defaults(resolved_fact_module, self._task, mod_args, self._templar)
return mod_args
def _combine_task_result(self, result: dict[str, t.Any], task_result: dict[str, t.Any]) -> dict[str, t.Any]:
""" builds the final result to return """
filtered_res = {
'ansible_facts': task_result.get('ansible_facts', {}),
'warnings': task_result.get('warnings', []),
'deprecations': task_result.get('deprecations', []),
}
# on conflict the last plugin processed wins, but try to do deep merge and append to lists.
return merge_hash(result, filtered_res, list_merge='append_rp')
def _handle_smart(self, modules: list, task_vars: dict[str, t.Any]):
""" Updates the module list when 'smart' is used, lookup network os mappings or use setup, warn when things seem inconsistent """
if 'smart' not in modules:
return
modules.pop(modules.index('smart')) # remove as this will cause 'module not found' errors
network_os = self._task.args.get('network_os', task_vars.get('ansible_network_os', task_vars.get('ansible_facts', {}).get('network_os')))
if network_os:
connection_map = C.config.get_config_value('CONNECTION_FACTS_MODULES', variables=task_vars)
if network_os in connection_map:
modules.append(connection_map[network_os])
elif not modules:
raise AnsibleActionFail(f"No fact modules available and we could not find a fact module for your network OS ({network_os}), "
"try setting one via the `FACTS_MODULES` configuration.")
if set(modules).intersection(set(C._ACTION_SETUP)):
# most don't realize how setup works with networking connection plugins (forced_local)
self._display.warning("Detected 'setup' module and a network OS is set, the output when running it will reflect 'localhost'"
" and not the target when a networking connection plugin is used.")
elif not set(modules).intersection(set(C._ACTION_SETUP)):
# no network OS and setup not in list, add setup by default since 'smart'
modules.append('ansible.legacy.setup')
def run(self, tmp: t.Optional[str] = None, task_vars: t.Optional[dict[str, t.Any]] = None) -> dict[str, t.Any]:
result = super(ActionModule, self).run(tmp, task_vars)
result['ansible_facts'] = {}
# copy the value with list() so we don't mutate the config
modules = list(C.config.get_config_value('FACTS_MODULES', variables=task_vars))
self._handle_smart(modules, task_vars)
parallel = task_vars.pop('ansible_facts_parallel', self._task.args.pop('parallel', None))
failed = {}
skipped = {}
if parallel is None:
if len(modules) > 1:
parallel = True
else:
parallel = False
else:
parallel = boolean(parallel)
timeout = self._task.args.get('gather_timeout', None)
async_val = self._task.async_val
if not parallel:
# serially execute each module
for fact_module in modules:
# just one module, no need for fancy async
mod_args = self._get_module_args(fact_module, task_vars)
# TODO: use gather_timeout to cut module execution if module itself does not support gather_timeout
res = self._execute_module(module_name=fact_module, module_args=mod_args, task_vars=task_vars, wrap_async=False)
if res.get('failed', False):
failed[fact_module] = res
elif res.get('skipped', False):
skipped[fact_module] = res
else:
result = self._combine_task_result(result, res)
self._remove_tmp_path(self._connection._shell.tmpdir)
else:
# do it async, aka parallel
jobs = {}
for fact_module in modules:
mod_args = self._get_module_args(fact_module, task_vars)
# if module does not handle timeout, use timeout to handle module, hijack async_val as this is what async_wrapper uses
# TODO: make this action complain about async/async settings, use parallel option instead .. or remove parallel in favor of async settings?
if timeout and 'gather_timeout' not in mod_args:
self._task.async_val = int(timeout)
elif async_val != 0:
self._task.async_val = async_val
else:
self._task.async_val = 0
self._display.vvvv("Running %s" % fact_module)
jobs[fact_module] = (self._execute_module(module_name=fact_module, module_args=mod_args, task_vars=task_vars, wrap_async=True))
while jobs:
for module in jobs:
poll_args = {'jid': jobs[module]['ansible_job_id'], '_async_dir': os.path.dirname(jobs[module]['results_file'])}
res = self._execute_module(module_name='ansible.legacy.async_status', module_args=poll_args, task_vars=task_vars, wrap_async=False)
if res.get('finished', False):
if res.get('failed', False):
failed[module] = res
elif res.get('skipped', False):
skipped[module] = res
else:
result = self._combine_task_result(result, res)
del jobs[module]
break
else:
time.sleep(0.1)
else:
time.sleep(0.5)
# restore value for post processing
if self._task.async_val != async_val:
self._task.async_val = async_val
if skipped:
result['msg'] = f"The following modules were skipped: {', '.join(skipped.keys())}."
result['skipped_modules'] = skipped
if len(skipped) == len(modules):
result['skipped'] = True
if failed:
result['failed_modules'] = failed
result.update(_error_utils.result_dict_from_captured_errors(
msg=f"The following modules failed to execute: {', '.join(failed.keys())}.",
errors=[r['exception'] for r in failed.values()],
))
# tell executor facts were gathered
result['ansible_facts']['_ansible_facts_gathered'] = True
# hack to keep --verbose from showing all the setup module result
result['_ansible_verbose_override'] = True
return result
|