Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ jobs:
python_version: '3.14'
- tox_env: py314-m_ans-ans13
python_version: '3.14'
- tox_env: py314-m_ans-ans14
python_version: '3.14'

- tox_env: py314-m_ans-ans13-s_lin
- tox_env: py314-m_ans-ans14-s_lin
python_version: '3.14'

- tox_env: py314-m_mtg
Expand Down Expand Up @@ -171,17 +173,17 @@ jobs:
name: macos ${{ matrix.tox_env }}
# https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
runs-on: macos-15
timeout-minutes: 15
timeout-minutes: 20
env:
MITOGEN_TEST_SKIP_CONTAINER_TESTS: 1

strategy:
fail-fast: false
matrix:
include:
- tox_env: py314-m_lcl-ans13
- tox_env: py314-m_lcl-ans14
python_version: '3.14'
- tox_env: py314-m_lcl-ans13-s_lin
- tox_env: py314-m_lcl-ans14-s_lin
python_version: '3.14'

- tox_env: py314-m_mtg
Expand Down
95 changes: 95 additions & 0 deletions ansible_mitogen/compat/six.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# SPDX-FileCopyrightText: 2010-2024 Benjamin Peterson
# SPDX-FileCopyrightText: 2026 Mitogen authors <https://github.com/mitogen-hq>
# SPDX-License-Identifier: MIT
# Source: https://github.com/benjaminp/six/blob/1.17.0/six.py
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import absolute_import

import sys
import types

if sys.version_info >= (3, 3):
from shlex import quote as shlex_quote
else:
from pipes import quote as shlex_quote


if sys.version_info >= (3, 0):
exec_ = getattr(__import__('builtins'), 'exec')

string_types = (str,)

def reraise(tp, value, tb=None):
try:
if value is None:
value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
finally:
value = None
tb = None

else:
string_types = (basestring,)

def exec_(_code_, _globs_=None, _locs_=None):
"""Execute code in a namespace."""
if _globs_ is None:
frame = sys._getframe(1)
_globs_ = frame.f_globals
if _locs_ is None:
_locs_ = frame.f_locals
del frame
elif _locs_ is None:
_locs_ = _globs_
exec("""exec _code_ in _globs_, _locs_""")

exec_("""def reraise(tp, value, tb=None):
try:
raise tp, value, tb
finally:
tb = None
""")


def with_metaclass(meta, *bases):
"""Create a base class with a metaclass."""
# This requires a bit of explanation: the basic idea is to make a dummy
# metaclass for one level of class instantiation that replaces itself with
# the actual metaclass.
class metaclass(type):

def __new__(cls, name, this_bases, d):
if sys.version_info[:2] >= (3, 7):
# This version introduced PEP 560 that requires a bit
# of extra care (we mimic what is done by __build_class__).
resolved_bases = types.resolve_bases(bases)
if resolved_bases is not bases:
d['__orig_bases__'] = bases
else:
resolved_bases = bases
return meta(name, resolved_bases, d)

@classmethod
def __prepare__(cls, name, this_bases):
return meta.__prepare__(name, bases)
return type.__new__(metaclass, 'temporary_class', (), {})
68 changes: 33 additions & 35 deletions ansible_mitogen/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@
import ansible.plugins.action
import ansible.utils.unsafe_proxy
import ansible.vars.clean

from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible.module_utils.six.moves import shlex_quote

import mitogen.core
import mitogen.select
Expand All @@ -52,7 +50,7 @@
import ansible_mitogen.target
import ansible_mitogen.utils
import ansible_mitogen.utils.unsafe

from ansible_mitogen.compat.six import shlex_quote

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -373,7 +371,10 @@ def _execute_module(self, module_name=None, module_args=None, tmp=None,
self._connection.context = None

self._connection._connect()
result = ansible_mitogen.planner.invoke(

# Ansible <= 13 (ansible-core <= 2.20): dict
# Ansible >= 14 (ansible-core >= 2.21): UnifiedTaskResult
task_result = ansible_mitogen.planner.invoke(
ansible_mitogen.planner.Invocation(
action=self,
connection=self._connection,
Expand All @@ -393,59 +394,56 @@ def _execute_module(self, module_name=None, module_args=None, tmp=None,
self._remove_tmp_path(tmp)

# prevents things like discovered_interpreter_* or ansible_discovered_interpreter_* from being set
ansible.vars.clean.remove_internal_keys(result)
try:
task_result.remove_internal_keys()
except AttributeError:
ansible.vars.clean.remove_internal_keys(task_result)

# taken from _execute_module of ansible 2.8.6
# propagate interpreter discovery results back to the controller
if self._discovered_interpreter_key:
if result.get('ansible_facts') is None:
result['ansible_facts'] = {}

# only cache discovered_interpreter if we're not running a rediscovery
# rediscovery happens in places like docker connections that could have different
# python interpreters than the main host
if not self._mitogen_rediscovered_interpreter:
result['ansible_facts'][self._discovered_interpreter_key] = self._discovered_interpreter
di_key = self._discovered_interpreter_key
di_val = self._discovered_interpreter
try:
task_result.set_fact(di_key, di_val)
except AttributeError:
task_result.setdefault('ansible_facts', {})[di_key] = di_val

discovery_warnings = getattr(self, '_discovery_warnings', [])
if discovery_warnings:
if result.get('warnings') is None:
result['warnings'] = []
result['warnings'].extend(discovery_warnings)
try:
task_result._extend_warnings(discovery_warnings)
except AttributeError:
task_result.setdefault('warnings', []).extend(discovery_warnings)

discovery_deprecation_warnings = getattr(self, '_discovery_deprecation_warnings', [])
if discovery_deprecation_warnings:
if result.get('deprecations') is None:
result['deprecations'] = []
result['deprecations'].extend(discovery_deprecation_warnings)
try:
task_result._extend_deprecations(discovery_deprecation_warnings)
except AttributeError:
task_result.setdefault('deprecations', []).extend(discovery_deprecation_warnings)

return ansible.utils.unsafe_proxy.wrap_var(result)
if ansible_mitogen.utils.ansible_version[:2] >= (2, 21):
task_result = task_result.as_result_dict(for_round_trip=True)
return ansible.utils.unsafe_proxy.wrap_var(task_result)

def _postprocess_response(self, result):
"""
Apply fixups mimicking ActionBase._execute_module(); this is copied
verbatim from action/__init__.py, the guts of _parse_returned_data are
garbage and should be removed or reimplemented once tests exist.

:param dict result:
Dictionary with format::

{
"rc": int,
"stdout": "stdout data",
"stderr": "stderr data"
}
"""
if ansible_mitogen.utils.ansible_version[:2] >= (2, 19):
data = self._parse_returned_data(result, profile='legacy')
else:
data = self._parse_returned_data(result)

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

return data

Expand Down
4 changes: 3 additions & 1 deletion ansible_mitogen/plugins/action/mitogen_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@

import os
import base64

from ansible.errors import AnsibleError, AnsibleActionFail, AnsibleActionSkip
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible.module_utils.six import string_types
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display
from ansible.utils.hashing import checksum, checksum_s, md5, secure_hash
from ansible.utils.path import makedirs_safe, is_subpath

from ansible_mitogen.compat.six import string_types

display = Display()


Expand Down
3 changes: 2 additions & 1 deletion ansible_mitogen/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@
import imp

import ansible.module_utils.common.warnings
from ansible.module_utils.six.moves import shlex_quote

import mitogen.core
import ansible_mitogen.target # TODO: circular import
from mitogen.core import to_text

from ansible_mitogen.compat.six import shlex_quote

try:
# Cannot use cStringIO as it does not support Unicode.
from StringIO import StringIO
Expand Down
3 changes: 1 addition & 2 deletions ansible_mitogen/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,14 @@

import ansible.constants

from ansible.module_utils.six import reraise

import mitogen.core
import mitogen.service
import ansible_mitogen.loaders
import ansible_mitogen.module_finder
import ansible_mitogen.target
import ansible_mitogen.utils
import ansible_mitogen.utils.unsafe
from ansible_mitogen.compat.six import reraise


LOG = logging.getLogger(__name__)
Expand Down
11 changes: 8 additions & 3 deletions ansible_mitogen/transport_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,21 @@
import ansible.constants as C
import ansible.executor.interpreter_discovery
import ansible.utils.unsafe_proxy

from ansible.module_utils.six import with_metaclass
from ansible.module_utils.parsing.convert_bool import boolean

import ansible_mitogen.utils
from ansible_mitogen.compat.six import with_metaclass

import mitogen.core


LOG = logging.getLogger(__name__)

if ansible_mitogen.utils.ansible_version[:2] >= (2, 21):
_INTERPRETER_DISCOVERY_MODES = frozenset(['auto', 'auto_silent'])
else:
_INTERPRETER_DISCOVERY_MODES = frozenset(['auto', 'auto_legacy', 'auto_silent', 'auto_legacy_silent'])

if ansible_mitogen.utils.ansible_version[:2] >= (2, 19):
_FALLBACK_INTERPRETER = ansible.executor.interpreter_discovery._FALLBACK_INTERPRETER
elif ansible_mitogen.utils.ansible_version[:2] >= (2, 17):
Expand All @@ -98,7 +103,7 @@ def run_interpreter_discovery_if_necessary(s, candidates, task_vars, action, red
if action._mitogen_discovering_interpreter:
return action._mitogen_interpreter_candidate

if s in ['auto', 'auto_legacy', 'auto_silent', 'auto_legacy_silent']:
if s in _INTERPRETER_DISCOVERY_MODES:
# python is the only supported interpreter_name as of Ansible 2.8.8
interpreter_name = 'python'
discovered_interpreter_config = u'discovered_interpreter_%s' % interpreter_name
Expand Down
4 changes: 3 additions & 1 deletion docs/ansible_detailed.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ Noteworthy Differences
+-----------------+ 3.11 - 3.14 |
| 12 | |
+-----------------+-----------------+
| 13 | 3.12 - 3.14 |
| 13 | |
+-----------------+ 3.12 - 3.14 |
| 14 | |
+-----------------+-----------------+

Verify your installation is running one of these versions by checking
Expand Down
12 changes: 12 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ To avail of fixes in an unreleased version, please download a ZIP file
`directly from GitHub <https://github.com/mitogen-hq/mitogen/>`_.


v0.3.49 (2026-06-03)
--------------------

* :gh:issue:`1523` :mod:`ansible_mitogen`: First Ansible 14 support
* :gh:issue:`1518` :mod:`mitogen`: Fix sudo authentication when the translated
password prompt doesn't contain U+003A COLON
* :gh:issue:`1523` tests: Split auto, auto_legacy, auto_legacy_silent
interpreter discovery tests
* :gh:issue:`1385` :mod:`ansible_mitogen`: Replace imports of deprecated
``ansible.module_utils.six``


v0.3.48 (2026-05-22)
--------------------

Expand Down
2 changes: 1 addition & 1 deletion mitogen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@


#: Library version as a tuple.
__version__ = (0, 3, 48)
__version__ = (0, 3, 49)


#: This is :data:`False` in slave contexts. Previously it was used to prevent
Expand Down
5 changes: 3 additions & 2 deletions mitogen/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@

import logging
import re
import sys

try:
if sys.version_info >= (3, 3):
from shlex import quote as shlex_quote
except ImportError:
else:
from pipes import quote as shlex_quote

import mitogen.core
Expand Down
Loading