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