Skip to content

Commit 09ea40a

Browse files
authored
Merge pull request #1524 from moreati/ansible14
Ansible 14 compatibility
2 parents 11dd968 + 86409b0 commit 09ea40a

14 files changed

Lines changed: 170 additions & 128 deletions

File tree

.github/workflows/tests.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,10 @@ jobs:
118118
python_version: '3.14'
119119
- tox_env: py314-m_ans-ans13
120120
python_version: '3.14'
121+
- tox_env: py314-m_ans-ans14
122+
python_version: '3.14'
121123

122-
- tox_env: py314-m_ans-ans13-s_lin
124+
- tox_env: py314-m_ans-ans14-s_lin
123125
python_version: '3.14'
124126

125127
- tox_env: py314-m_mtg
@@ -171,17 +173,17 @@ jobs:
171173
name: macos ${{ matrix.tox_env }}
172174
# https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
173175
runs-on: macos-15
174-
timeout-minutes: 15
176+
timeout-minutes: 20
175177
env:
176178
MITOGEN_TEST_SKIP_CONTAINER_TESTS: 1
177179

178180
strategy:
179181
fail-fast: false
180182
matrix:
181183
include:
182-
- tox_env: py314-m_lcl-ans13
184+
- tox_env: py314-m_lcl-ans14
183185
python_version: '3.14'
184-
- tox_env: py314-m_lcl-ans13-s_lin
186+
- tox_env: py314-m_lcl-ans14-s_lin
185187
python_version: '3.14'
186188

187189
- tox_env: py314-m_mtg

ansible_mitogen/mixins.py

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,10 @@ def _execute_module(self, module_name=None, module_args=None, tmp=None,
373373
self._connection.context = None
374374

375375
self._connection._connect()
376-
result = ansible_mitogen.planner.invoke(
376+
377+
# Ansible <= 13 (ansible-core <= 2.20): dict
378+
# Ansible >= 14 (ansible-core >= 2.21): UnifiedTaskResult
379+
task_result = ansible_mitogen.planner.invoke(
377380
ansible_mitogen.planner.Invocation(
378381
action=self,
379382
connection=self._connection,
@@ -393,59 +396,56 @@ def _execute_module(self, module_name=None, module_args=None, tmp=None,
393396
self._remove_tmp_path(tmp)
394397

395398
# prevents things like discovered_interpreter_* or ansible_discovered_interpreter_* from being set
396-
ansible.vars.clean.remove_internal_keys(result)
399+
try:
400+
task_result.remove_internal_keys()
401+
except AttributeError:
402+
ansible.vars.clean.remove_internal_keys(task_result)
397403

398404
# taken from _execute_module of ansible 2.8.6
399405
# propagate interpreter discovery results back to the controller
400406
if self._discovered_interpreter_key:
401-
if result.get('ansible_facts') is None:
402-
result['ansible_facts'] = {}
403-
404407
# only cache discovered_interpreter if we're not running a rediscovery
405408
# rediscovery happens in places like docker connections that could have different
406409
# python interpreters than the main host
407410
if not self._mitogen_rediscovered_interpreter:
408-
result['ansible_facts'][self._discovered_interpreter_key] = self._discovered_interpreter
411+
di_key = self._discovered_interpreter_key
412+
di_val = self._discovered_interpreter
413+
try:
414+
task_result.set_fact(di_key, di_val)
415+
except AttributeError:
416+
task_result.setdefault('ansible_facts', {})[di_key] = di_val
409417

410418
discovery_warnings = getattr(self, '_discovery_warnings', [])
411419
if discovery_warnings:
412-
if result.get('warnings') is None:
413-
result['warnings'] = []
414-
result['warnings'].extend(discovery_warnings)
420+
try:
421+
task_result._extend_warnings(discovery_warnings)
422+
except AttributeError:
423+
task_result.setdefault('warnings', []).extend(discovery_warnings)
415424

416425
discovery_deprecation_warnings = getattr(self, '_discovery_deprecation_warnings', [])
417426
if discovery_deprecation_warnings:
418-
if result.get('deprecations') is None:
419-
result['deprecations'] = []
420-
result['deprecations'].extend(discovery_deprecation_warnings)
427+
try:
428+
task_result._extend_deprecations(discovery_deprecation_warnings)
429+
except AttributeError:
430+
task_result.setdefault('deprecations', []).extend(discovery_deprecation_warnings)
421431

422-
return ansible.utils.unsafe_proxy.wrap_var(result)
432+
if ansible_mitogen.utils.ansible_version[:2] >= (2, 21):
433+
task_result = task_result.as_result_dict(for_round_trip=True)
434+
return ansible.utils.unsafe_proxy.wrap_var(task_result)
423435

424436
def _postprocess_response(self, result):
425-
"""
426-
Apply fixups mimicking ActionBase._execute_module(); this is copied
427-
verbatim from action/__init__.py, the guts of _parse_returned_data are
428-
garbage and should be removed or reimplemented once tests exist.
429-
430-
:param dict result:
431-
Dictionary with format::
432-
433-
{
434-
"rc": int,
435-
"stdout": "stdout data",
436-
"stderr": "stderr data"
437-
}
438-
"""
439437
if ansible_mitogen.utils.ansible_version[:2] >= (2, 19):
440438
data = self._parse_returned_data(result, profile='legacy')
441439
else:
442440
data = self._parse_returned_data(result)
443441

444-
# Cutpasted from the base implementation.
445-
if 'stdout' in data and 'stdout_lines' not in data:
446-
data['stdout_lines'] = (data['stdout'] or u'').splitlines()
447-
if 'stderr' in data and 'stderr_lines' not in data:
448-
data['stderr_lines'] = (data['stderr'] or u'').splitlines()
442+
# ansible-core >= 2.21: done in UnifiedTaskResult.as_result_dict()
443+
if ansible_mitogen.utils.ansible_version[:2] <= (2, 20):
444+
# Cutpasted from the base implementation.
445+
if 'stdout' in data and 'stdout_lines' not in data:
446+
data['stdout_lines'] = (data['stdout'] or u'').splitlines()
447+
if 'stderr' in data and 'stderr_lines' not in data:
448+
data['stderr_lines'] = (data['stderr'] or u'').splitlines()
449449

450450
return data
451451

ansible_mitogen/transport_config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@
7979

8080
LOG = logging.getLogger(__name__)
8181

82+
if ansible_mitogen.utils.ansible_version[:2] >= (2, 21):
83+
_INTERPRETER_DISCOVERY_MODES = frozenset(['auto', 'auto_silent'])
84+
else:
85+
_INTERPRETER_DISCOVERY_MODES = frozenset(['auto', 'auto_legacy', 'auto_silent', 'auto_legacy_silent'])
86+
8287
if ansible_mitogen.utils.ansible_version[:2] >= (2, 19):
8388
_FALLBACK_INTERPRETER = ansible.executor.interpreter_discovery._FALLBACK_INTERPRETER
8489
elif ansible_mitogen.utils.ansible_version[:2] >= (2, 17):
@@ -98,7 +103,7 @@ def run_interpreter_discovery_if_necessary(s, candidates, task_vars, action, red
98103
if action._mitogen_discovering_interpreter:
99104
return action._mitogen_interpreter_candidate
100105

101-
if s in ['auto', 'auto_legacy', 'auto_silent', 'auto_legacy_silent']:
106+
if s in _INTERPRETER_DISCOVERY_MODES:
102107
# python is the only supported interpreter_name as of Ansible 2.8.8
103108
interpreter_name = 'python'
104109
discovered_interpreter_config = u'discovered_interpreter_%s' % interpreter_name

docs/ansible_detailed.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ Noteworthy Differences
145145
+-----------------+ 3.11 - 3.14 |
146146
| 12 | |
147147
+-----------------+-----------------+
148-
| 13 | 3.12 - 3.14 |
148+
| 13 | |
149+
+-----------------+ 3.12 - 3.14 |
150+
| 14 | |
149151
+-----------------+-----------------+
150152

151153
Verify your installation is running one of these versions by checking

docs/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ To avail of fixes in an unreleased version, please download a ZIP file
2121
In progress (unreleased)
2222
------------------------
2323

24+
* :gh:issue:`1523` :mod:`ansible_mitogen`: First Ansible 14 support
2425
* :gh:issue:`1518` :mod:`mitogen`: Fix sudo authentication when the translated
2526
password prompt doesn't contain U+003A COLON
27+
* :gh:issue:`1523` tests: Split auto, auto_legacy, auto_legacy_silent
28+
interpreter discovery tests
2629

2730

2831
v0.3.48 (2026-05-22)

tests/ansible/integration/async/result_shell_echo_hi.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
tasks:
66

77
- name: Async shell
8+
vars:
9+
# Ansible >= 14 (ansible-core >= 2.21) omits the "invocation" key by
10+
# default, https://github.com/ansible/ansible/pull/86771
11+
ansible_inject_invocation: true
812
shell: echo hi; echo there >&2
913
async: 100
1014
poll: 0
@@ -26,6 +30,7 @@
2630
- async_out.cmd == "echo hi; echo there >&2"
2731
- 'async_out.delta.startswith("0:00:")'
2832
- async_out.end.startswith("20")
33+
- async_out.invocation is defined
2934
- async_out.invocation.module_args._raw_params == "echo hi; echo there >&2"
3035
- async_out.invocation.module_args._uses_shell == True
3136
- async_out.invocation.module_args.chdir == None

tests/ansible/integration/async/runner_with_polling_and_timeout.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
job1.msg == "async task did not complete within the requested time" or
2222
job1.msg == "async task did not complete within the requested time - 1s" or
2323
job1.msg == "Job reached maximum time limit of 1 seconds."
24+
or (
25+
ansible_version_major_minor is version('2.21', '>=', strict=True)
26+
and job1.msg == "async task produced unparsable results"
27+
)
2428
fail_msg: |
2529
job1={{ job1 }}
2630
tags:
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
- import_playbook: complex_args.yml
2-
- import_playbook: ansible_2_8_tests.yml
2+
- import_playbook: auto.yml
3+
- import_playbook: auto_legacy.yml
4+
- import_playbook: auto_legacy_silent.yml

tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml renamed to tests/ansible/integration/interpreter_discovery/auto.yml

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# ripped and ported from https://github.com/ansible/ansible/pull/50163/files, when interpreter discovery was added to ansible
2-
---
3-
- name: integration/interpreter_discovery/ansible_2_8_tests.yml, baseline
2+
- name: integration/interpreter_discovery/auto.yml, linear
43
hosts: test-targets
54
strategy: linear
65
tasks:
@@ -12,7 +11,7 @@
1211
register: linear_auto_result
1312

1413

15-
- name: integration/interpreter_discovery/ansible_2_8_tests.yml
14+
- name: integration/interpreter_discovery/auto.yml
1615
hosts: test-targets
1716
gather_facts: true
1817
tasks:
@@ -69,48 +68,6 @@
6968
or not (echoout.running_python.sys.platform == "darwin"
7069
and echoout.running_python.platform.release.major == 20)
7170

72-
73-
- name: test that auto_legacy gives a dep warning when /usr/bin/python present but != auto result
74-
block:
75-
- name: clear facts to force interpreter discovery to run
76-
meta: clear_facts
77-
78-
- name: trigger discovery with auto_legacy
79-
vars:
80-
ansible_python_interpreter: auto_legacy
81-
ping:
82-
register: legacy
83-
84-
- name: check for dep warning (only on platforms where auto result is not /usr/bin/python and legacy is) for ansible 2.8-2.11
85-
# from ansible 2.12 on this changed
86-
# - https://docs.ansible.com/ansible/devel/porting_guides/porting_guide_5.html#python-interpreter-discovery
87-
# - https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html
88-
# default discovery method is now auto and will default to python3
89-
# and the message changed from a deprecation warning to a real warning that can not be suppressed by
90-
# using deprecation_warnings=False
91-
assert:
92-
that:
93-
- legacy.deprecations | default([]) | length > 0
94-
fail_msg: |
95-
legacy={{ legacy }}
96-
# only check for a dep warning if legacy returned /usr/bin/python and auto didn't
97-
when:
98-
- legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python'
99-
- auto_out.ansible_facts.discovered_interpreter_python != '/usr/bin/python'
100-
- ansible_version_major_minor is version('2.12', '<', strict=True)
101-
102-
- name: check for warning (only on platforms where auto result is not /usr/bin/python and legacy is) from ansible 2.12 on
103-
assert:
104-
that:
105-
- legacy.warnings | default([]) | length > 0
106-
fail_msg: |
107-
legacy={{ legacy }}
108-
# only check for a warning if legacy returned /usr/bin/python and auto didn't
109-
when:
110-
- legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python'
111-
- auto_out.ansible_facts.discovered_interpreter_python != '/usr/bin/python'
112-
- ansible_version_major_minor is version('2.12', '>=', strict=True)
113-
11471
- name: test that auto_silent never warns and got the same answer as auto
11572
block:
11673
- name: clear facts to force interpreter discovery to run
@@ -129,25 +86,6 @@
12986
fail_msg: |
13087
auto_silent_out={{ auto_silent_out }}
13188
132-
133-
- name: test that auto_legacy_silent never warns and got the same answer as auto_legacy
134-
block:
135-
- name: clear facts to force interpreter discovery to run
136-
meta: clear_facts
137-
138-
- name: trigger discovery with auto_legacy_silent
139-
vars:
140-
ansible_python_interpreter: auto_legacy_silent
141-
ping:
142-
register: legacy_silent
143-
144-
- assert:
145-
that:
146-
- legacy_silent.warnings is not defined
147-
- legacy_silent.ansible_facts.discovered_interpreter_python == legacy.ansible_facts.discovered_interpreter_python
148-
fail_msg: |
149-
legacy_silent={{ legacy_silent }}
150-
15189
- name: ensure modules can't set discovered_interpreter_X or ansible_X_interpreter
15290
block:
15391
- test_echo_module:
@@ -180,5 +118,3 @@
180118
- meta: clear_facts
181119
when:
182120
- ansible_version_major_minor is version('2.8', '>=', strict=True)
183-
tags:
184-
- ansible_2_8_tests
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
- name: integration/interpreter_discovery/auto_legacy.yml
2+
hosts: test-targets
3+
gather_facts: true
4+
tasks:
5+
- meta: end_play
6+
when:
7+
# Ansible 14 (ansible-core 2.21) removed auto_legacy & auto_legacy_silent
8+
- ansible_version_major_minor is version('2.7', '<=', strict=True)
9+
or ansible_version_major_minor is version('2.21', '>=', strict=True)
10+
11+
- meta: clear_facts
12+
- name: Discover with auto
13+
vars:
14+
ansible_python_interpreter: auto
15+
ping:
16+
register: auto_result
17+
18+
- meta: clear_facts
19+
- name: Discover with auto_legacy
20+
vars:
21+
ansible_python_interpreter: auto_legacy
22+
ping:
23+
register: auto_legacy_result
24+
25+
- name: check for dep warning (only on platforms where auto result is not /usr/bin/python and legacy is) for ansible 2.8-2.11
26+
# from ansible 2.12 on this changed
27+
# - https://docs.ansible.com/ansible/devel/porting_guides/porting_guide_5.html#python-interpreter-discovery
28+
# - https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html
29+
# default discovery method is now auto and will default to python3
30+
# and the message changed from a deprecation warning to a real warning that can not be suppressed by
31+
# using deprecation_warnings=False
32+
assert:
33+
that:
34+
- auto_legacy_result.deprecations | default([]) | length > 0
35+
fail_msg: |
36+
auto_legacy_result={{ auto_legacy_result }}
37+
# only check for a dep warning if auto_legacy returned /usr/bin/python and auto didn't
38+
when:
39+
- ansible_version_major_minor is version('2.12', '<', strict=True)
40+
- auto_legacy_result.ansible_facts.discovered_interpreter_python == '/usr/bin/python'
41+
- auto_result.ansible_facts.discovered_interpreter_python != '/usr/bin/python'
42+
43+
- name: check for warning (only on platforms where auto result is not /usr/bin/python and legacy is) from ansible 2.12 on
44+
assert:
45+
that:
46+
- auto_legacy_result.warnings | default([]) | length > 0
47+
fail_msg: |
48+
auto_legacy_result={{ auto_legacy_result }}
49+
# only check for a warning if auto_legacy returned /usr/bin/python and auto didn't
50+
when:
51+
- ansible_version_major_minor is version('2.12', '>=', strict=True)
52+
- auto_legacy_result.ansible_facts.discovered_interpreter_python == '/usr/bin/python'
53+
- auto_result.ansible_facts.discovered_interpreter_python != '/usr/bin/python'

0 commit comments

Comments
 (0)