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
|
# Copyright: (c) 2018 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 re
from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.utils.display import Display
from ansible.utils.plugin_docs import get_versioned_doclink
_FALLBACK_INTERPRETER = '/usr/bin/python3'
display = Display()
foundre = re.compile(r'FOUND(.*)ENDFOUND', flags=re.DOTALL)
class InterpreterDiscoveryRequiredError(Exception):
def __init__(self, message, interpreter_name, discovery_mode):
super(InterpreterDiscoveryRequiredError, self).__init__(message)
self.interpreter_name = interpreter_name
self.discovery_mode = discovery_mode
def discover_interpreter(action, interpreter_name, discovery_mode, task_vars):
"""Probe the target host for a Python interpreter from the `INTERPRETER_PYTHON_FALLBACK` list, returning the first found or `/usr/bin/python3` if none."""
host = task_vars.get('inventory_hostname', 'unknown')
res = None
found_interpreters = [_FALLBACK_INTERPRETER] # fallback value
is_silent = discovery_mode.endswith('_silent')
if discovery_mode.startswith('auto_legacy'):
display.deprecated(
msg=f"The '{discovery_mode}' option for 'INTERPRETER_PYTHON' now has the same effect as 'auto'.",
version='2.21',
)
try:
bootstrap_python_list = C.config.get_config_value('INTERPRETER_PYTHON_FALLBACK', variables=task_vars)
display.vvv(msg=f"Attempting {interpreter_name} interpreter discovery.", host=host)
# not all command -v impls accept a list of commands, so we have to call it once per python
command_list = ["command -v '%s'" % py for py in bootstrap_python_list]
shell_bootstrap = "echo FOUND; {0}; echo ENDFOUND".format('; '.join(command_list))
# FUTURE: in most cases we probably don't want to use become, but maybe sometimes we do?
res = action._low_level_execute_command(shell_bootstrap, sudoable=False)
raw_stdout = res.get('stdout', u'')
match = foundre.match(raw_stdout)
if not match:
display.debug(u'raw interpreter discovery output: {0}'.format(raw_stdout), host=host)
raise ValueError('unexpected output from Python interpreter discovery')
found_interpreters = [interp.strip() for interp in match.groups()[0].splitlines() if interp.startswith('/')]
display.debug(u"found interpreters: {0}".format(found_interpreters), host=host)
if not found_interpreters:
if not is_silent:
display.warning(msg=f'No python interpreters found for host {host!r} (tried {bootstrap_python_list!r}).')
# this is lame, but returning None or throwing an exception is uglier
return _FALLBACK_INTERPRETER
except AnsibleError:
raise
except Exception as ex:
if not is_silent:
display.error_as_warning(msg=f'Unhandled error in Python interpreter discovery for host {host!r}.', exception=ex)
if res and res.get('stderr'): # the current ssh plugin implementation always has stderr, making coverage of the false case difficult
display.vvv(msg=f"Interpreter discovery remote stderr:\n{res.get('stderr')}", host=host)
if not is_silent:
display.warning(
msg=(
f"Host {host!r} is using the discovered Python interpreter at {found_interpreters[0]!r}, "
"but future installation of another Python interpreter could cause a different interpreter to be discovered."
),
help_text=f"See {get_versioned_doclink('reference_appendices/interpreter_discovery.html')} for more information.",
)
return found_interpreters[0]
|