1# Copyright: (c) 2021, 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 subprocess 9import sys 10 11from ansible.module_utils.common.text.converters import to_bytes, to_native 12 13 14def has_respawned(): 15 return hasattr(sys.modules['__main__'], '_respawned') 16 17 18def respawn_module(interpreter_path): 19 """ 20 Respawn the currently-running Ansible Python module under the specified Python interpreter. 21 22 Ansible modules that require libraries that are typically available only under well-known interpreters 23 (eg, ``yum``, ``apt``, ``dnf``) can use bespoke logic to determine the libraries they need are not 24 available, then call `respawn_module` to re-execute the current module under a different interpreter 25 and exit the current process when the new subprocess has completed. The respawned process inherits only 26 stdout/stderr from the current process. 27 28 Only a single respawn is allowed. ``respawn_module`` will fail on nested respawns. Modules are encouraged 29 to call `has_respawned()` to defensively guide behavior before calling ``respawn_module``, and to ensure 30 that the target interpreter exists, as ``respawn_module`` will not fail gracefully. 31 32 :arg interpreter_path: path to a Python interpreter to respawn the current module 33 """ 34 35 if has_respawned(): 36 raise Exception('module has already been respawned') 37 38 # FUTURE: we need a safe way to log that a respawn has occurred for forensic/debug purposes 39 payload = _create_payload() 40 stdin_read, stdin_write = os.pipe() 41 os.write(stdin_write, to_bytes(payload)) 42 os.close(stdin_write) 43 rc = subprocess.call([interpreter_path, '--'], stdin=stdin_read) 44 sys.exit(rc) # pylint: disable=ansible-bad-function 45 46 47def probe_interpreters_for_module(interpreter_paths, module_name): 48 """ 49 Probes a supplied list of Python interpreters, returning the first one capable of 50 importing the named module. This is useful when attempting to locate a "system 51 Python" where OS-packaged utility modules are located. 52 53 :arg interpreter_paths: iterable of paths to Python interpreters. The paths will be probed 54 in order, and the first path that exists and can successfully import the named module will 55 be returned (or ``None`` if probing fails for all supplied paths). 56 :arg module_name: fully-qualified Python module name to probe for (eg, ``selinux``) 57 """ 58 for interpreter_path in interpreter_paths: 59 if not os.path.exists(interpreter_path): 60 continue 61 try: 62 rc = subprocess.call([interpreter_path, '-c', 'import {0}'.format(module_name)]) 63 if rc == 0: 64 return interpreter_path 65 except Exception: 66 continue 67 68 return None 69 70 71def _create_payload(): 72 from ansible.module_utils import basic 73 smuggled_args = getattr(basic, '_ANSIBLE_ARGS') 74 if not smuggled_args: 75 raise Exception('unable to access ansible.module_utils.basic._ANSIBLE_ARGS (not launched by AnsiballZ?)') 76 module_fqn = sys.modules['__main__']._module_fqn 77 modlib_path = sys.modules['__main__']._modlib_path 78 respawn_code_template = ''' 79import runpy 80import sys 81 82module_fqn = '{module_fqn}' 83modlib_path = '{modlib_path}' 84smuggled_args = b"""{smuggled_args}""".strip() 85 86 87if __name__ == '__main__': 88 sys.path.insert(0, modlib_path) 89 90 from ansible.module_utils import basic 91 basic._ANSIBLE_ARGS = smuggled_args 92 93 runpy.run_module(module_fqn, init_globals=dict(_respawned=True), run_name='__main__', alter_sys=True) 94 ''' 95 96 respawn_code = respawn_code_template.format(module_fqn=module_fqn, modlib_path=modlib_path, smuggled_args=to_native(smuggled_args)) 97 98 return respawn_code 99