Skip to content

Commit a5fe4a9

Browse files
authored
Merge pull request #658 from s1113950/complexAnsiblePythonInterpreterArg
Adds support for special ansible_python_interpreter values, ansible_python_interpreter discovery, and fixes tests
2 parents c289b16 + 957e295 commit a5fe4a9

35 files changed

+867
-304
lines changed

.ci/ansible_tests.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ def pause_if_interactive():
6666
ci_lib.dump_file(inventory_path)
6767

6868
if not ci_lib.exists_in_path('sshpass'):
69+
# fix errors with apt-get update
70+
run("sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 78BD65473CB3BD13")
71+
run("sudo sed -i -e 's#deb https://downloads.apache.org/cassandra/debian 39x main#deb http://downloads.apache.org/cassandra/debian 39x main#g' /etc/apt/sources.list.d/cassandra.list")
72+
6973
run("sudo apt-get update")
7074
run("sudo apt-get install -y sshpass")
7175

.ci/azure-pipelines-steps.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@ steps:
1414
# stuff into. The virtualenv can probably be removed again, but this was a
1515
# hard-fought battle and for now I am tired of this crap.
1616
- script: |
17-
sudo ln -fs /usr/bin/python$(python.version) /usr/bin/python
18-
/usr/bin/python -m pip install -U virtualenv setuptools wheel
19-
/usr/bin/python -m virtualenv /tmp/venv -p /usr/bin/python$(python.version)
17+
# need wheel before building virtualenv because of bdist_wheel and setuptools deps
18+
# Mac's System Integrity Protection prevents symlinking /usr/bin
19+
# and Azure isn't allowing disabling it apparently: https://developercommunityapi.westus.cloudapp.azure.com/idea/558702/allow-disabling-sip-on-microsoft-hosted-macos-agen.html
20+
# the || will activate when running python3 tests
21+
# TODO: get python3 tests passing
22+
(sudo ln -fs /usr/bin/python$(python.version) /usr/bin/python &&
23+
/usr/bin/python -m pip install -U pip wheel setuptools &&
24+
/usr/bin/python -m pip install -U virtualenv &&
25+
/usr/bin/python -m virtualenv /tmp/venv -p /usr/bin/python$(python.version)) ||
26+
(sudo /usr/bin/python$(python.version) -m pip install -U pip wheel setuptools &&
27+
/usr/bin/python$(python.version) -m venv /tmp/venv)
2028
echo "##vso[task.prependpath]/tmp/venv/bin"
2129
2230
displayName: activate venv

.ci/azure-pipelines.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,21 @@ jobs:
99
steps:
1010
- template: azure-pipelines-steps.yml
1111
pool:
12-
vmImage: macOS-10.13
12+
vmImage: macOS-10.14
1313
strategy:
1414
matrix:
1515
Mito27_27:
1616
python.version: '2.7'
1717
MODE: mitogen
18-
Ans280_27:
18+
Ans288_27:
1919
python.version: '2.7'
2020
MODE: localhost_ansible
21+
VER: 2.8.8
2122

2223

2324
- job: Linux
2425
pool:
25-
vmImage: "Ubuntu 16.04"
26+
vmImage: "Ubuntu 18.04"
2627
steps:
2728
- template: azure-pipelines-steps.yml
2829
strategy:
@@ -45,10 +46,6 @@ jobs:
4546
MODE: mitogen
4647
DISTRO: centos6
4748

48-
#
49-
#
50-
#
51-
5249
#Py26CentOS7:
5350
#python.version: '2.7'
5451
#MODE: mitogen

.ci/localhost_ansible_tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@
4444

4545
if os.path.expanduser('~mitogen__user1') == '~mitogen__user1':
4646
os.chdir(IMAGE_PREP_DIR)
47-
run("ansible-playbook -c local -i localhost, _user_accounts.yml")
47+
run("ansible-playbook -c local -i localhost, _user_accounts.yml -vvv")
4848

4949

5050
with ci_lib.Fold('ansible'):
5151
os.chdir(TESTS_DIR)
5252
playbook = os.environ.get('PLAYBOOK', 'all.yml')
53-
run('./run_ansible_playbook.py %s -l target %s',
53+
run('./run_ansible_playbook.py %s -l target %s -vvv',
5454
playbook, ' '.join(sys.argv[1:]))

ansible_mitogen/connection.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def _connect_docker(spec):
183183
'kwargs': {
184184
'username': spec.remote_user(),
185185
'container': spec.remote_addr(),
186-
'python_path': spec.python_path(),
186+
'python_path': spec.python_path(rediscover_python=True),
187187
'connect_timeout': spec.ansible_ssh_timeout() or spec.timeout(),
188188
'remote_name': get_remote_name(spec),
189189
}
@@ -503,6 +503,9 @@ class Connection(ansible.plugins.connection.ConnectionBase):
503503
#: matching vanilla Ansible behaviour.
504504
loader_basedir = None
505505

506+
# set by `_get_task_vars()` for interpreter discovery
507+
_action = None
508+
506509
def __del__(self):
507510
"""
508511
Ansible cannot be trusted to always call close() e.g. the synchronize
@@ -551,6 +554,23 @@ def _get_task_vars(self):
551554
connection passed into any running action.
552555
"""
553556
if self._task_vars is not None:
557+
# check for if self._action has already been set or not
558+
# there are some cases where the ansible executor passes in task_vars
559+
# so we don't walk the stack to find them
560+
# TODO: is there a better way to get the ActionModuleMixin object?
561+
# ansible python discovery needs it to run discover_interpreter()
562+
if not isinstance(self._action, ansible_mitogen.mixins.ActionModuleMixin):
563+
f = sys._getframe()
564+
while f:
565+
if f.f_code.co_name == 'run':
566+
f_self = f.f_locals.get('self')
567+
if isinstance(f_self, ansible_mitogen.mixins.ActionModuleMixin):
568+
self._action = f_self
569+
break
570+
elif f.f_code.co_name == '_execute_meta':
571+
break
572+
f = f.f_back
573+
554574
return self._task_vars
555575

556576
f = sys._getframe()
@@ -559,6 +579,9 @@ def _get_task_vars(self):
559579
f_locals = f.f_locals
560580
f_self = f_locals.get('self')
561581
if isinstance(f_self, ansible_mitogen.mixins.ActionModuleMixin):
582+
# backref for python interpreter discovery, should be safe because _get_task_vars
583+
# is always called before running interpreter discovery
584+
self._action = f_self
562585
task_vars = f_locals.get('task_vars')
563586
if task_vars:
564587
LOG.debug('recovered task_vars from Action')
@@ -600,16 +623,33 @@ def get_task_var(self, key, default=None):
600623
does not make sense to extract connection-related configuration for the
601624
delegated-to machine from them.
602625
"""
626+
def _fetch_task_var(task_vars, key):
627+
"""
628+
Special helper func in case vars can be templated
629+
"""
630+
SPECIAL_TASK_VARS = [
631+
'ansible_python_interpreter'
632+
]
633+
if key in task_vars:
634+
val = task_vars[key]
635+
if '{' in str(val) and key in SPECIAL_TASK_VARS:
636+
# template every time rather than storing in a cache
637+
# in case a different template value is used in a different task
638+
val = self.templar.template(
639+
val,
640+
preserve_trailing_newlines=True,
641+
escape_backslashes=False
642+
)
643+
return val
644+
603645
task_vars = self._get_task_vars()
604646
if self.delegate_to_hostname is None:
605-
if key in task_vars:
606-
return task_vars[key]
647+
return _fetch_task_var(task_vars, key)
607648
else:
608649
delegated_vars = task_vars['ansible_delegated_vars']
609650
if self.delegate_to_hostname in delegated_vars:
610651
task_vars = delegated_vars[self.delegate_to_hostname]
611-
if key in task_vars:
612-
return task_vars[key]
652+
return _fetch_task_var(task_vars, key)
613653

614654
return default
615655

@@ -654,6 +694,8 @@ def _spec_from_via(self, proxied_inventory_name, via_spec):
654694
inventory_name=inventory_name,
655695
play_context=self._play_context,
656696
host_vars=dict(via_vars), # TODO: make it lazy
697+
task_vars=self._get_task_vars(), # needed for interpreter discovery in parse_python_path
698+
action=self._action,
657699
become_method=become_method or None,
658700
become_user=become_user or None,
659701
)
@@ -847,6 +889,18 @@ def reset(self):
847889
self.reset_compat_msg
848890
)
849891

892+
# Strategy's _execute_meta doesn't have an action obj but we'll need one for
893+
# running interpreter_discovery
894+
# will create a new temporary action obj for this purpose
895+
self._action = ansible_mitogen.mixins.ActionModuleMixin(
896+
task=0,
897+
connection=self,
898+
play_context=self._play_context,
899+
loader=0,
900+
templar=0,
901+
shared_loader_obj=0
902+
)
903+
850904
# Clear out state in case we were ever connected.
851905
self.close()
852906

ansible_mitogen/mixins.py

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@
6060
except ImportError:
6161
from ansible.vars.unsafe_proxy import wrap_var
6262

63+
try:
64+
# ansible 2.8 moved remove_internal_keys to the clean module
65+
from ansible.vars.clean import remove_internal_keys
66+
except ImportError:
67+
try:
68+
from ansible.vars.manager import remove_internal_keys
69+
except ImportError:
70+
# ansible 2.3.3 has remove_internal_keys as a protected func on the action class
71+
# we'll fallback to calling self._remove_internal_keys in this case
72+
remove_internal_keys = lambda a: "Not found"
73+
6374

6475
LOG = logging.getLogger(__name__)
6576

@@ -108,6 +119,16 @@ def __init__(self, task, connection, *args, **kwargs):
108119
if not isinstance(connection, ansible_mitogen.connection.Connection):
109120
_, self.__class__ = type(self).__bases__
110121

122+
# required for python interpreter discovery
123+
connection.templar = self._templar
124+
self._finding_python_interpreter = False
125+
self._rediscovered_python = False
126+
# redeclaring interpreter discovery vars here in case running ansible < 2.8.0
127+
self._discovered_interpreter_key = None
128+
self._discovered_interpreter = False
129+
self._discovery_deprecation_warnings = []
130+
self._discovery_warnings = []
131+
111132
def run(self, tmp=None, task_vars=None):
112133
"""
113134
Override run() to notify Connection of task-specific data, so it has a
@@ -370,6 +391,34 @@ def _execute_module(self, module_name=None, module_args=None, tmp=None,
370391
# on _execute_module().
371392
self._remove_tmp_path(tmp)
372393

394+
# prevents things like discovered_interpreter_* or ansible_discovered_interpreter_* from being set
395+
# handle ansible 2.3.3 that has remove_internal_keys in a different place
396+
check = remove_internal_keys(result)
397+
if check == 'Not found':
398+
self._remove_internal_keys(result)
399+
400+
# taken from _execute_module of ansible 2.8.6
401+
# propagate interpreter discovery results back to the controller
402+
if self._discovered_interpreter_key:
403+
if result.get('ansible_facts') is None:
404+
result['ansible_facts'] = {}
405+
406+
# only cache discovered_interpreter if we're not running a rediscovery
407+
# rediscovery happens in places like docker connections that could have different
408+
# python interpreters than the main host
409+
if not self._rediscovered_python:
410+
result['ansible_facts'][self._discovered_interpreter_key] = self._discovered_interpreter
411+
412+
if self._discovery_warnings:
413+
if result.get('warnings') is None:
414+
result['warnings'] = []
415+
result['warnings'].extend(self._discovery_warnings)
416+
417+
if self._discovery_deprecation_warnings:
418+
if result.get('deprecations') is None:
419+
result['deprecations'] = []
420+
result['deprecations'].extend(self._discovery_deprecation_warnings)
421+
373422
return wrap_var(result)
374423

375424
def _postprocess_response(self, result):
@@ -407,17 +456,54 @@ def _low_level_execute_command(self, cmd, sudoable=True, in_data=None,
407456
"""
408457
LOG.debug('_low_level_execute_command(%r, in_data=%r, exe=%r, dir=%r)',
409458
cmd, type(in_data), executable, chdir)
459+
410460
if executable is None: # executable defaults to False
411461
executable = self._play_context.executable
412462
if executable:
413463
cmd = executable + ' -c ' + shlex_quote(cmd)
414464

415-
rc, stdout, stderr = self._connection.exec_command(
416-
cmd=cmd,
417-
in_data=in_data,
418-
sudoable=sudoable,
419-
mitogen_chdir=chdir,
420-
)
465+
# TODO: HACK: if finding python interpreter then we need to keep
466+
# calling exec_command until we run into the right python we'll use
467+
# chicken-and-egg issue, mitogen needs a python to run low_level_execute_command
468+
# which is required by Ansible's discover_interpreter function
469+
if self._finding_python_interpreter:
470+
possible_pythons = [
471+
'/usr/bin/python',
472+
'python3',
473+
'python3.7',
474+
'python3.6',
475+
'python3.5',
476+
'python2.7',
477+
'python2.6',
478+
'/usr/libexec/platform-python',
479+
'/usr/bin/python3',
480+
'python'
481+
]
482+
else:
483+
# not used, just adding a filler value
484+
possible_pythons = ['python']
485+
486+
def _run_cmd():
487+
return self._connection.exec_command(
488+
cmd=cmd,
489+
in_data=in_data,
490+
sudoable=sudoable,
491+
mitogen_chdir=chdir,
492+
)
493+
494+
for possible_python in possible_pythons:
495+
try:
496+
self._possible_python_interpreter = possible_python
497+
rc, stdout, stderr = _run_cmd()
498+
# TODO: what exception is thrown?
499+
except:
500+
# we've reached the last python attempted and failed
501+
# TODO: could use enumerate(), need to check which version of python first had it though
502+
if possible_python == 'python':
503+
raise
504+
else:
505+
continue
506+
421507
stdout_text = to_text(stdout, errors=encoding_errors)
422508

423509
return {

ansible_mitogen/planner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ def _get_planner(name, path, source):
535535

536536
def invoke(invocation):
537537
"""
538-
Find a Planner subclass corresnding to `invocation` and use it to invoke
538+
Find a Planner subclass corresponding to `invocation` and use it to invoke
539539
the module.
540540
541541
:param Invocation invocation:

ansible_mitogen/plugins/action/mitogen_get_stack.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ def run(self, tmp=None, task_vars=None):
5252
'changed': True,
5353
'result': stack,
5454
'_ansible_verbose_always': True,
55+
# for ansible < 2.8, we'll default to /usr/bin/python like before
56+
'discovered_interpreter': self._connection._action._discovered_interpreter
5557
}

0 commit comments

Comments
 (0)