88 lines
3.8 KiB
Python
88 lines
3.8 KiB
Python
# 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]
|