From a595ac2f2cbcff0d0f04b43b8516b8bdb1d5a108 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 21 Apr 2025 15:01:00 -0400 Subject: [PATCH] package, allow to use actions over modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add ability to override/set package managers added collection specified action plugin Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- lib/ansible/config/base.yml | 7 +++ lib/ansible/plugins/action/package.py | 71 +++++++++++++++++++-------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml index 5c64ea67107..5eaa306b95c 100644 --- a/lib/ansible/config/base.yml +++ b/lib/ansible/config/base.yml @@ -1822,6 +1822,13 @@ OLD_PLUGIN_CACHE_CLEARING: type: boolean default: False version_added: "2.8" +PACKAGE_MANAGERS: + name: Map of package facts to actions. + description: A dictionary mapping the detected pkg_mgr fact to the action that should resolve it. + default: {} + type: dict + vars: + - name: ansible_package_managers PAGER: name: pager application to use default: less diff --git a/lib/ansible/plugins/action/package.py b/lib/ansible/plugins/action/package.py index e5042c95f27..3fed45fa8d3 100644 --- a/lib/ansible/plugins/action/package.py +++ b/lib/ansible/plugins/action/package.py @@ -16,6 +16,7 @@ # along with Ansible. If not, see . from __future__ import annotations +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.facts.system.pkg_mgr import PKG_MGRS @@ -32,7 +33,7 @@ class ActionModule(ActionBase): BUILTIN_PKG_MGR_MODULES = {manager['name'] for manager in PKG_MGRS} - def run(self, tmp=None, task_vars=None): + def run(self, tmp: str | None = None, task_vars: dict | None = None) -> dict: """ handler for package operations """ self._supports_check_mode = True @@ -40,10 +41,10 @@ class ActionModule(ActionBase): super(ActionModule, self).run(tmp, task_vars) - module = self._task.args.get('use', 'auto') + action = self._task.args.get('use', 'auto') try: - if module == 'auto': + if action == 'auto': if self._task.delegate_to: hosts_vars = task_vars['hostvars'][self._task.delegate_to] @@ -53,9 +54,9 @@ class ActionModule(ActionBase): tvars = task_vars # use config - module = tvars.get('ansible_package_use', None) + action = tvars.get('ansible_package_use', None) - if not module: + if not action: # no use, no config, get from facts if hosts_vars.get('ansible_facts', {}).get('pkg_mgr', False): facts = hosts_vars @@ -77,29 +78,57 @@ class ActionModule(ActionBase): try: # actually get from facts - module = facts['ansible_facts'][pmgr] + action = facts['ansible_facts'][pmgr] except KeyError: raise AnsibleActionFail('Could not detect a package manager. Try using the "use" option.') - if module and module != 'auto': - if not self._shared_loader_obj.module_loader.has_plugin(module): - raise AnsibleActionFail('Could not find a matching action for the "%s" package manager.' % module) - else: - # run the 'package' module - new_module_args = self._task.args.copy() - if 'use' in new_module_args: - del new_module_args['use'] + if action and action != 'auto': + module_context = None + # see if we use custom mapped, use orig if not + action = C.PACKAGE_MANAGERS.get(action, action) + # prefix with ansible.legacy to eliminate external collisions while still allowing library/ override + if action in self.BUILTIN_PKG_MGR_MODULES: + action = f'ansible.legacy.{action}' + + # find what to execute, action plugins having priority + has_action_plugin = self._shared_loader_obj.module_loader.has_plugin(action) + if not has_action_plugin: + module_context = self._shared_loader_obj.module_loader.find_plugin_with_context(action, collection_list=self._task.collections) + if module_context and module_context.resolved and module_context.action_plugin: + # module itself specifies action plugin + action = module_context.action_plugin + has_action_plugin = True + + # prep to run he action + new_module_args = self._task.args.copy() + if 'use' in new_module_args: + del new_module_args['use'] + + if has_action_plugin: + display.vvvv(f"Chose {action!r} action plugin") + new_task = new_task = self._task.copy() + new_task.args.update(new_module_args) + pkg_action, action_context = self._shared_loader_obj.action_loader.get_with_context(action, + task=new_task, + connection=self._connection, + play_context=self._play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj) + if pkg_action: + display.vvvv(f"Running {action!r}") + return pkg_action.run(task_vars=task_vars) + else: + raise AnsibleActionFail(f"Failed to load {action!r}: {action_context!r}") + elif module_context and module_context.resolved: + display.vvvv(f"Chose {action!r} module") # get defaults for specific module - context = self._shared_loader_obj.module_loader.find_plugin_with_context(module, collection_list=self._task.collections) - new_module_args = _apply_action_arg_defaults(context.resolved_fqcn, self._task, new_module_args, self._templar) + new_module_args = _apply_action_arg_defaults(module_context.resolved_fqcn, self._task, new_module_args, self._templar) - if module in self.BUILTIN_PKG_MGR_MODULES: - # prefix with ansible.legacy to eliminate external collisions while still allowing library/ override - module = 'ansible.legacy.' + module - display.vvvv("Running %s" % module) - return self._execute_module(module_name=module, module_args=new_module_args, task_vars=task_vars, wrap_async=self._task.async_val) + display.vvvv(f"Running {action!r}") + return self._execute_module(module_name=action, module_args=new_module_args, task_vars=task_vars, wrap_async=self._task.async_val) else: raise AnsibleActionFail('Could not detect which package manager to use. Try gathering facts or setting the "use" option.') finally: