From 1368bfa34821df099fde36c6e655d92d40bf24f5 Mon Sep 17 00:00:00 2001 From: Matt Davis <6775756+nitzmahone@users.noreply.github.com> Date: Wed, 27 Jul 2022 14:55:50 -0700 Subject: [PATCH] PluginLoader now installs module-to-be-imported in sys.modules before exec (as Python import does). (#78364) --- changelogs/fragments/self_referential.yml | 2 ++ lib/ansible/plugins/loader.py | 13 ++++++++- .../normal/action_plugins/self_referential.py | 29 +++++++++++++++++++ .../plugin_loader/normal/self_referential.yml | 5 ++++ 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/self_referential.yml create mode 100644 test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py create mode 100644 test/integration/targets/plugin_loader/normal/self_referential.yml diff --git a/changelogs/fragments/self_referential.yml b/changelogs/fragments/self_referential.yml new file mode 100644 index 00000000000..a6467e8fb0a --- /dev/null +++ b/changelogs/fragments/self_referential.yml @@ -0,0 +1,2 @@ +bugfixes: + - Fix PluginLoader to mimic Python import machinery by adding module to sys.modules before exec diff --git a/lib/ansible/plugins/loader.py b/lib/ansible/plugins/loader.py index 35f16b2fbbc..85cdaf8281c 100644 --- a/lib/ansible/plugins/loader.py +++ b/lib/ansible/plugins/loader.py @@ -796,11 +796,22 @@ class PluginLoader: return sys.modules[full_name] with warnings.catch_warnings(): + # FIXME: this still has issues if the module was previously imported but not "cached", + # we should bypass this entire codepath for things that are directly importable warnings.simplefilter("ignore", RuntimeWarning) spec = importlib.util.spec_from_file_location(to_native(full_name), to_native(path)) module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) + + # mimic import machinery; make the module-being-loaded available in sys.modules during import + # and remove if there's a failure... sys.modules[full_name] = module + + try: + spec.loader.exec_module(module) + except Exception: + del sys.modules[full_name] + raise + return module def _update_object(self, obj, name, path, redirected_names=None): diff --git a/test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py b/test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py new file mode 100644 index 00000000000..b4c89577597 --- /dev/null +++ b/test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py @@ -0,0 +1,29 @@ +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.action import ActionBase + +import sys + +# reference our own module from sys.modules while it's being loaded to ensure the importer behaves properly +try: + mod = sys.modules[__name__] +except KeyError: + raise Exception(f'module {__name__} is not accessible via sys.modules, likely a pluginloader bug') + + +class ActionModule(ActionBase): + TRANSFERS_FILES = False + + def run(self, tmp=None, task_vars=None): + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + result['changed'] = False + result['msg'] = 'self-referential action loaded and ran successfully' + return result diff --git a/test/integration/targets/plugin_loader/normal/self_referential.yml b/test/integration/targets/plugin_loader/normal/self_referential.yml new file mode 100644 index 00000000000..d3eed218333 --- /dev/null +++ b/test/integration/targets/plugin_loader/normal/self_referential.yml @@ -0,0 +1,5 @@ +- hosts: testhost + gather_facts: false + tasks: + - name: ensure a self-referential action plugin loads properly + self_referential: