diff --git a/.ci/install_sshpass b/.ci/install_sshpass new file mode 100755 index 00000000..05d7ebd6 --- /dev/null +++ b/.ci/install_sshpass @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +VERSION="$1" + +curl \ + --fail \ + --location \ + --no-progress-meter \ + --remote-name \ + "https://downloads.sourceforge.net/project/sshpass/sshpass/${VERSION}/sshpass-${VERSION}.tar.gz" +tar xvf "sshpass-${VERSION}.tar.gz" +cd "sshpass-${VERSION}" +./configure +sudo make install diff --git a/.ci/localhost_ansible_tests.py b/.ci/localhost_ansible_tests.py index 359dc195..9203f120 100755 --- a/.ci/localhost_ansible_tests.py +++ b/.ci/localhost_ansible_tests.py @@ -17,17 +17,6 @@ with ci_lib.Fold('unit_tests'): with ci_lib.Fold('job_setup'): os.chmod(ci_lib.TESTS_SSH_PRIVATE_KEY_FILE, int('0600', 8)) - # NOTE: sshpass v1.06 causes errors so pegging to 1.05 -> "msg": "Error when changing password","out": "passwd: DS error: eDSAuthFailed\n", - # there's a checksum error with "brew install http://git.io/sshpass.rb" though, so installing manually - if not ci_lib.exists_in_path('sshpass'): - subprocess.check_call( - "curl -O -L https://sourceforge.net/projects/sshpass/files/sshpass/1.05/sshpass-1.05.tar.gz && \ - tar xvf sshpass-1.05.tar.gz && \ - cd sshpass-1.05 && \ - ./configure && \ - sudo make install", - shell=True, - ) with ci_lib.Fold('machine_prep'): diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f6a6871..ac00d84b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -177,10 +177,16 @@ jobs: - name: Ans_313_11 python_version: '3.13' tox_env: py313-mode_ansible-ansible11 + - name: Ans_313_12 + python_version: '3.13' + tox_env: py313-mode_ansible-ansible12 - name: Van_313_11 python_version: '3.13' tox_env: py313-mode_ansible-ansible11-strategy_linear + - name: Van_313_12 + python_version: '3.13' + tox_env: py313-mode_ansible-ansible12-strategy_linear - name: Mito_313 python_version: '3.13' @@ -268,11 +274,19 @@ jobs: tox_env: py313-mode_mitogen - name: Loc_313_11 + sshpass_version: "1.10" tox_env: py313-mode_localhost-ansible11 - name: Van_313_11 + sshpass_version: "1.10" tox_env: py313-mode_localhost-ansible11-strategy_linear + - name: Loc_313_12 + tox_env: py313-mode_localhost-ansible12 + + - name: Van_313_12 + tox_env: py313-mode_localhost-ansible12-strategy_linear + steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -305,6 +319,8 @@ jobs: # GitHub macOS 12 images: python2.7 is installed, but not on $PATH echo "/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7: sys.executable: $(/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7 -c 'import sys; print(sys.executable)')" fi + - run: .ci/install_sshpass ${{ matrix.sshpass_version }} + if: ${{ matrix.sshpass_version }} - name: Install tooling run: | set -o errexit -o nounset -o pipefail diff --git a/.gitignore b/.gitignore index 7297d720..43e46a19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.ansible/ .coverage .tox .venv diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 5053a5f5..03856f4d 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -147,7 +147,7 @@ def _connect_ssh(spec): 'ssh_path': spec.ssh_executable(), 'connect_timeout': spec.timeout(), 'ssh_args': spec.ssh_args(), - 'ssh_debug_level': spec.mitogen_ssh_debug_level(), + 'ssh_debug_level': spec.verbosity(), 'remote_name': get_remote_name(spec), 'keepalive_count': ( spec.mitogen_ssh_keepalive_count() or 10 @@ -767,7 +767,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): C.BECOME_ALLOW_SAME_USER): stack += (CONNECTION_METHOD[spec.become_method()](spec),) - return stack + return ansible_mitogen.utils.unsafe.cast(stack) def _build_stack(self): """ diff --git a/ansible_mitogen/loaders.py b/ansible_mitogen/loaders.py index 123dd4ac..9597e3ee 100644 --- a/ansible_mitogen/loaders.py +++ b/ansible_mitogen/loaders.py @@ -49,7 +49,7 @@ __all__ = [ ANSIBLE_VERSION_MIN = (2, 10) -ANSIBLE_VERSION_MAX = (2, 18) +ANSIBLE_VERSION_MAX = (2, 19) NEW_VERSION_MSG = ( "Your Ansible version (%s) is too recent. The most recent version\n" diff --git a/ansible_mitogen/mixins.py b/ansible_mitogen/mixins.py index dadf2c17..8ddbb437 100644 --- a/ansible_mitogen/mixins.py +++ b/ansible_mitogen/mixins.py @@ -29,6 +29,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +import json import logging import os import pwd @@ -42,7 +43,6 @@ 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 -from ansible.parsing.utils.jsonify import jsonify import mitogen.core import mitogen.select @@ -219,8 +219,13 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase): Used by the base _execute_module(), and in <2.4 also by the template action module, and probably others. """ + if data is None and ansible_mitogen.utils.ansible_version[:2] <= (2, 18): + data = '{}' if isinstance(data, dict): - data = jsonify(data) + try: + data = json.dumps(data, ensure_ascii=False) + except UnicodeDecodeError: + data = json.dumps(data) if not isinstance(data, bytes): data = to_bytes(data, errors='surrogate_or_strict') @@ -402,15 +407,17 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase): if not self._mitogen_rediscovered_interpreter: result['ansible_facts'][self._discovered_interpreter_key] = self._discovered_interpreter - if self._discovery_warnings: + discovery_warnings = getattr(self, '_discovery_warnings', []) + if discovery_warnings: if result.get('warnings') is None: result['warnings'] = [] - result['warnings'].extend(self._discovery_warnings) + result['warnings'].extend(discovery_warnings) - if self._discovery_deprecation_warnings: + discovery_deprecation_warnings = getattr(self, '_discovery_deprecation_warnings', []) + if discovery_deprecation_warnings: if result.get('deprecations') is None: result['deprecations'] = [] - result['deprecations'].extend(self._discovery_deprecation_warnings) + result['deprecations'].extend(discovery_deprecation_warnings) return ansible.utils.unsafe_proxy.wrap_var(result) @@ -429,7 +436,10 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase): "stderr": "stderr data" } """ - data = self._parse_returned_data(result) + 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: diff --git a/ansible_mitogen/planner.py b/ansible_mitogen/planner.py index 2915f4b7..5d0fbd78 100644 --- a/ansible_mitogen/planner.py +++ b/ansible_mitogen/planner.py @@ -170,6 +170,7 @@ class Planner(object): """ binding = self._inv.connection.get_binding() + kwargs = ansible_mitogen.utils.unsafe.cast(kwargs) new = dict((mitogen.core.UnicodeType(k), kwargs[k]) for k in kwargs) new.setdefault('good_temp_dir', @@ -204,7 +205,7 @@ class BinaryPlanner(Planner): module=self._inv.module_name, path=self._inv.module_path, json_args=json.dumps(self._inv.module_args), - env=self._inv.env, + env=ansible_mitogen.utils.unsafe.cast(self._inv.env), **kwargs ) @@ -546,7 +547,7 @@ def _invoke_async_task(invocation, planner): call_recv = context.call_async( ansible_mitogen.target.run_module_async, job_id=job_id, - timeout_secs=invocation.timeout_secs, + timeout_secs=ansible_mitogen.utils.unsafe.cast(invocation.timeout_secs), started_sender=started_recv.to_sender(), kwargs=planner.get_kwargs(), ) diff --git a/ansible_mitogen/runner.py b/ansible_mitogen/runner.py index b60e537c..ce7dceb9 100644 --- a/ansible_mitogen/runner.py +++ b/ansible_mitogen/runner.py @@ -73,6 +73,7 @@ except ImportError: from io import StringIO # Prevent accidental import of an Ansible module from hanging on stdin read. +# FIXME Should probably be b'{}' or None. Ansible 2.19 has bytes | None = None. import ansible.module_utils.basic ansible.module_utils.basic._ANSIBLE_ARGS = '{}' @@ -635,6 +636,7 @@ class NewStyleStdio(object): sys.stderr = StringIO() encoded = json.dumps({'ANSIBLE_MODULE_ARGS': args}) ansible.module_utils.basic._ANSIBLE_ARGS = utf8(encoded) + ansible.module_utils.basic._ANSIBLE_PROFILE = 'legacy' sys.stdin = StringIO(mitogen.core.to_text(encoded)) self.original_get_path = getattr(ansible.module_utils.basic, @@ -649,7 +651,9 @@ class NewStyleStdio(object): sys.stdout = self.original_stdout sys.stderr = self.original_stderr sys.stdin = self.original_stdin + # FIXME Should probably be b'{}' or None. Ansible 2.19 has bytes | None = None. ansible.module_utils.basic._ANSIBLE_ARGS = '{}' + ansible.module_utils.basic._ANSIBLE_PROFILE = None class ProgramRunner(Runner): diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index abc0e379..a48ab757 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -57,6 +57,7 @@ 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 @@ -338,7 +339,12 @@ class ContextService(mitogen.service.Service): 'ansible_mitogen.target', 'mitogen.fork', 'mitogen.service', - ) + ) + (( + 'ansible.module_utils._internal._json._profiles._module_legacy_c2m', + 'ansible.module_utils._internal._json._profiles._module_legacy_m2c', + 'ansible.module_utils._internal._json._profiles._module_modern_c2m', + 'ansible.module_utils._internal._json._profiles._module_legacy_m2c', + ) if ansible_mitogen.utils.ansible_version[:2] >= (2, 19) else ()) def _send_module_forwards(self, context): if hasattr(self.router.responder, 'forward_modules'): diff --git a/ansible_mitogen/transport_config.py b/ansible_mitogen/transport_config.py index 22afd197..3d5fac3c 100644 --- a/ansible_mitogen/transport_config.py +++ b/ansible_mitogen/transport_config.py @@ -406,6 +406,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)): Value of "ansible_doas_exe" variable. """ + @abc.abstractmethod + def verbosity(self): + """ + How verbose to make logging or diagnostics output. + """ + class PlayContextSpec(Spec): """ @@ -601,6 +607,17 @@ class PlayContextSpec(Spec): os.environ.get('ANSIBLE_DOAS_EXE') ) + def verbosity(self): + try: + verbosity = self._connection.get_option('verbosity', hostvars=self._task_vars) + except KeyError: + verbosity = self.mitogen_ssh_debug_level() + + if verbosity: + return int(verbosity) + + return 0 + class MitogenViaSpec(Spec): """ @@ -836,3 +853,13 @@ class MitogenViaSpec(Spec): self._host_vars.get('ansible_doas_exe') or os.environ.get('ANSIBLE_DOAS_EXE') ) + + def verbosity(self): + verbosity = self._host_vars.get('ansible_ssh_verbosity') + if verbosity is None: + verbosity = self.mitogen_ssh_debug_level() + + if verbosity: + return int(verbosity) + + return 0 diff --git a/ansible_mitogen/utils/unsafe.py b/ansible_mitogen/utils/unsafe.py index b2c3d533..a3aed462 100644 --- a/ansible_mitogen/utils/unsafe.py +++ b/ansible_mitogen/utils/unsafe.py @@ -16,8 +16,11 @@ __all__ = [ def _cast_to_dict(obj): return {cast(k): cast(v) for k, v in obj.items()} def _cast_to_list(obj): return [cast(v) for v in obj] +def _cast_to_set(obj): return set(cast(v) for v in obj) +def _cast_to_tuple(obj): return tuple(cast(v) for v in obj) def _cast_unsafe(obj): return obj._strip_unsafe() def _passthrough(obj): return obj +def _untag(obj): return obj._native_copy() # A dispatch table to cast objects based on their exact type. @@ -26,25 +29,64 @@ _CAST_DISPATCH = { bytes: bytes, dict: _cast_to_dict, list: _cast_to_list, - tuple: _cast_to_list, mitogen.core.UnicodeType: mitogen.core.UnicodeType, } _CAST_DISPATCH.update({t: _passthrough for t in mitogen.utils.PASSTHROUGH}) -if hasattr(ansible.utils.unsafe_proxy.AnsibleUnsafeText, '_strip_unsafe'): +_CAST_SUBTYPES = [ + dict, + list, +] + +if hasattr(ansible.utils.unsafe_proxy, 'TrustedAsTemplate'): + import datetime + import ansible.module_utils._internal._datatag + _CAST_DISPATCH.update({ + set: _cast_to_set, + tuple: _cast_to_tuple, + ansible.module_utils._internal._datatag._AnsibleTaggedBytes: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDate: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDateTime: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDict: _cast_to_dict, + ansible.module_utils._internal._datatag._AnsibleTaggedFloat: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedInt: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedList: _cast_to_list, + ansible.module_utils._internal._datatag._AnsibleTaggedSet: _cast_to_set, + ansible.module_utils._internal._datatag._AnsibleTaggedStr: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedTime: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedTuple: _cast_to_tuple, + ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes, + ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType, + datetime.date: _passthrough, + datetime.datetime: _passthrough, + datetime.time: _passthrough, + }) + _CAST_SUBTYPES.extend([ + set, + tuple, + ]) +elif hasattr(ansible.utils.unsafe_proxy.AnsibleUnsafeText, '_strip_unsafe'): _CAST_DISPATCH.update({ + tuple: _cast_to_list, ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: _cast_unsafe, ansible.utils.unsafe_proxy.AnsibleUnsafeText: _cast_unsafe, ansible.utils.unsafe_proxy.NativeJinjaUnsafeText: _cast_unsafe, }) + _CAST_SUBTYPES.extend([ + tuple, + ]) elif ansible_mitogen.utils.ansible_version[:2] <= (2, 16): _CAST_DISPATCH.update({ + tuple: _cast_to_list, ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes, ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType, }) + _CAST_SUBTYPES.extend([ + tuple, + ]) else: mitogen_ver = '.'.join(str(v) for v in mitogen.__version__) - raise ImportError("Mitogen %s can't unwrap Ansible %s AnsibleUnsafe objects" + raise ImportError("Mitogen %s can't cast Ansible %s objects" % (mitogen_ver, ansible.__version__)) @@ -73,7 +115,9 @@ def cast(obj): return unwrapper(obj) # Slow path: obj is some unknown subclass - if isinstance(obj, dict): return _cast_to_dict(obj) - if isinstance(obj, (list, tuple)): return _cast_to_list(obj) + for typ_ in _CAST_SUBTYPES: + if isinstance(obj, typ_): + unwrapper = _CAST_DISPATCH[typ_] + return unwrapper(obj) return mitogen.utils.cast(obj) diff --git a/docs/ansible_detailed.rst b/docs/ansible_detailed.rst index 3d80a290..06aa4955 100644 --- a/docs/ansible_detailed.rst +++ b/docs/ansible_detailed.rst @@ -141,7 +141,9 @@ Noteworthy Differences +-----------------+ 3.10 - 3.13 | | 10 | | +-----------------+-----------------+ - | 11 | 3.11 - 3.13 | + | 11 | | + +-----------------+ 3.11 - 3.13+ | + | 12 | | +-----------------+-----------------+ Verify your installation is running one of these versions by checking @@ -1016,6 +1018,8 @@ Like the :ans:conn:`ssh` except connection delegation is supported. * ``ansible_ssh_private_key_file`` * ``ansible_ssh_pass``, ``ansible_ssh_password``, ``ansible_password`` (default: assume passwordless) +* ``ansible_ssh_host_key_checking``, ``ansible_host_key_checking`` (default: + :data:`True`) * ``ssh_args``, ``ssh_common_args``, ``ssh_extra_args`` * ``mitogen_mask_remote_name``: if :data:`True`, mask the identity of the Ansible controller process on remote machines. To simplify diagnostics, diff --git a/docs/changelog.rst b/docs/changelog.rst index 4e3f703e..61b29bd4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,59 @@ To avail of fixes in an unreleased version, please download a ZIP file `directly from GitHub `_. +v0.3.25 (2025-07-29) +-------------------- + +Ansible 12 has deprecated third-party strategy plugins. This is currently +how Mitogen integrates with Ansible (e.g. `ANSIBLE_STRATEGY=mitogen_linear`). +Running Ansible 12 + Mitogen will currently print a deprecation warning + + [DEPRECATION WARNING]: Use of strategy plugins not included in + ansible.builtin are deprecated [...]. This feature will be removed from + ansible-core in a future release. + +Ansible + Mitogen will still work for now. Mitogen is considering alternatives +to strategy plugins under :gh:issue:`1278`. + +* :gh:issue:`1258` Ansible 12 (ansible-core 2.19) support + + +v0.3.25b1 (2025-07-21) +---------------------- + +* :gh:issue:`1303` CI: Switch to archived Debian 10 (buster) apt repository + + +v0.3.25a3 (2025-07-02) +---------------------- + +* :gh:issue:`1285` CI: use `result_format = yaml` for Ansible test output, + instead of deprecated `stdout_callback = yaml` +* :gh:issue:`1293` CI: Fix ``ansible_version`` comparisons when an Ansible + release candidate is under test +* :gh:issue:`1275` CI: Test ``ansible_ssh_password`` behaviour without + ``sshpass`` installed +* :gh:issue:`1282` :mod:`ansible_mitogen`: Support ``ANSIBLE_SSH_VERBOSITY`` + with Ansible 12 + + +v0.3.25a2 (2025-06-21) +---------------------- + +* :gh:issue:`1274` :mod:`ansible_mitogen`: Replace use of `jsonify()`, which + is deprecated form Ansible 12 (ansible-core 2.19) + + +v0.3.25a1 (2025-06-05) +---------------------- + +* :gh:issue:`1258` Initial Ansible 12 (ansible-core 2.19) support +* :gh:issue:`1258` :mod:`ansible_mitogen`: Initial Ansible datatag support + (:gh:anspull:`84621`) +* :gh:issue:`1258` :mod:`ansible_mitogen`: Ansible 12 (ansible-core 2.19) test + jobs + + v0.3.24 (2025-05-29) -------------------- diff --git a/docs/conf.py b/docs/conf.py index 9ad5b534..276a05d6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,8 +5,11 @@ sys.path.append('.') def changelog_version(path, encoding='utf-8'): + "Return the 1st *stable* (not pre, dev) version in the changelog" + # See also grep_version() in setup.py + # e.g. "0.1.2, (1999-12-31)\n" version_pattern = re.compile( - r'^v(?P[0-9]+\.[0-9]+\.[0-9]+)', + r'^v(?P\d+\.\d+\.\d+) \((?P\d\d\d\d-\d\d-\d\d)\)$', re.MULTILINE, ) @@ -64,6 +67,15 @@ domainrefs = { 'text': '#%s', 'url': 'https://github.com/mitogen-hq/mitogen/pull/%s', }, + 'gh:ansissue': { + 'text': 'Ansible #%s', + 'url': 'https://github.com/ansible/ansible/issues/%s', + }, + 'gh:anspull': { + 'text': 'Ansible #%s', + 'url': 'https://github.com/ansible/ansible/pull/%s', + }, + 'ans:mod': { 'text': '%s module', 'url': 'https://docs.ansible.com/ansible/latest/modules/%s_module.html', diff --git a/mitogen/__init__.py b/mitogen/__init__.py index 3fe380fa..7133b66c 100644 --- a/mitogen/__init__.py +++ b/mitogen/__init__.py @@ -35,7 +35,7 @@ be expected. On the slave, it is built dynamically during startup. #: Library version as a tuple. -__version__ = (0, 3, 24) +__version__ = (0, 3, 25) #: This is :data:`False` in slave contexts. Previously it was used to prevent diff --git a/setup.cfg b/setup.cfg index 08919787..ded55039 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bdist_wheel] -universal=1 +python_tag = py2.py3 [coverage:run] branch = true diff --git a/setup.py b/setup.py index 1639d383..f8aeffea 100644 --- a/setup.py +++ b/setup.py @@ -33,15 +33,34 @@ from setuptools import find_packages, setup def grep_version(): + # See also changlelog_version() in docs.conf.py path = os.path.join(os.path.dirname(__file__), 'mitogen/__init__.py') + + # Based on https://packaging.python.org/en/latest/specifications/version-specifiers/#appendix-parsing-version-strings-with-regular-expressions + # e.g. "__version__ = (0, 1, 2)", "__version__ = (0, 1, 3, 'dev')", + # "__version__ = (0, 1, 4, 'a', 1)" version_pattern = re.compile( - r"__version__ = \((\d+), (\d+), (\d+)(?:, '(dev)')?\)", + r''' + ^__version__\s=\s\( + (?P\d+) + ,\s + (?P\d+) + ,\s + (?P\d+) + (?: + (?:,\s '(?Pdev)') + | (?:,\s '(?Pa|b)' ,\s (?P\d+)) + )? + \) + $ + ''', + re.MULTILINE | re.VERBOSE, ) with open(path) as fp: match = version_pattern.search(fp.read()) if match is None: raise ValueError('Could not find __version__ string in %s', path) - # E.g. '0.1.2', '0.1.3dev' + # e.g. '0.1.2', '0.1.3dev', '0.1.4a1' return '.'.join(str(part) for part in match.groups() if part) diff --git a/tests/ansible/ansible.cfg b/tests/ansible/ansible.cfg index 48a4eec3..4060d9ea 100644 --- a/tests/ansible/ansible.cfg +++ b/tests/ansible/ansible.cfg @@ -1,5 +1,7 @@ [defaults] any_errors_fatal = true +# Ansible >= 6 (ansible-core >= 2.13) +callback_result_format = yaml # callbacks_enabled was added in Ansible 4 (ansible-core 2.11). # profile_tasks: Displays timing for each task and summary table of top N tasks # timer: Displays "Playbook run took 0 days, 0 hours, ..." @@ -17,13 +19,13 @@ strategy_plugins = ../../ansible_mitogen/plugins/strategy inventory_plugins = lib/inventory action_plugins = lib/action callback_plugins = lib/callback -stdout_callback = yaml vars_plugins = lib/vars library = lib/modules filter_plugins = lib/filters module_utils = lib/module_utils retry_files_enabled = False -show_task_path_on_failure = true # Added in ansible-core 2.11 +# Ansible >= 4 (ansible-core >= 2.11) +show_task_path_on_failure = true display_args_to_stdout = True forks = 100 diff --git a/tests/ansible/hosts/group_vars/all.yml b/tests/ansible/hosts/group_vars/all.yml index 6f518cad..7ec10d6a 100644 --- a/tests/ansible/hosts/group_vars/all.yml +++ b/tests/ansible/hosts/group_vars/all.yml @@ -1,4 +1,11 @@ --- +# Avoid `ansible_version.full is version(..., strict=True)` limitations. +# Pre-release info (alpha/beta/rc) is intentionally ignored. +# Behaviour that is present or expected in ansible-core 2.50.x should be +# tested even if ansible-core 2.50.0a1 or 2.50.0rc1 is under test. +ansible_version_major_minor: "{{ ansible_version.major }}.{{ ansible_version.minor }}" +ansible_version_major_minor_patch: "{{ ansible_version.major }}.{{ ansible_version.minor }}.{{ ansible_version.revision | regex_search('^[0-9]+') }}" + become_unpriv_available: >- {# Vanilla Ansible >= 4 (ansible-core >= 2.11) can use `setfacl` for @@ -13,7 +20,7 @@ become_unpriv_available: >- ( not is_mitogen and is_macos_controller - and ansible_version.full is version("2.11", ">=", strict=True) + and ansible_version_major_minor is version("2.11", ">=", strict=True) ) or ( is_mitogen @@ -22,7 +29,7 @@ become_unpriv_available: >- or ( is_mitogen and ansible_python_interpreter is not defined - and ansible_version.full is version("2.18", "<", strict=True) + and ansible_version_major_minor is version("2.18", "<", strict=True) ) -}} diff --git a/tests/ansible/hosts/group_vars/debian10.yml b/tests/ansible/hosts/group_vars/debian10.yml new file mode 100644 index 00000000..c6c0a268 --- /dev/null +++ b/tests/ansible/hosts/group_vars/debian10.yml @@ -0,0 +1,4 @@ +package_manager_repos: + - dest: /etc/apt/sources.list + content: | + deb http://archive.debian.org/debian buster main contrib non-free diff --git a/tests/ansible/hosts/group_vars/debian11.yml b/tests/ansible/hosts/group_vars/debian11.yml index 9f62f43c..30c98341 100644 --- a/tests/ansible/hosts/group_vars/debian11.yml +++ b/tests/ansible/hosts/group_vars/debian11.yml @@ -1,5 +1,7 @@ package_manager_keys: - src: debian-archive-bullseye-automatic.gpg # Debian 11 dest: /etc/apt/trusted.gpg.d/debian-archive-bullseye-automatic.gpg + - src: debian-archive-bullseye-security-automatic.asc # Debian 11 + dest: /etc/apt/trusted.gpg.d/debian-archive-bullseye-security-automatic.asc - src: debian-archive-bookworm-automatic.gpg # Debian 12 dest: /etc/apt/trusted.gpg.d/debian-archive-bookworm-automatic.gpg diff --git a/tests/ansible/integration/_expected_ssh_port.yml b/tests/ansible/integration/_expected_ssh_port.yml index 442659a5..f85ec3e2 100644 --- a/tests/ansible/integration/_expected_ssh_port.yml +++ b/tests/ansible/integration/_expected_ssh_port.yml @@ -11,8 +11,10 @@ - set_fact: expected_ssh_port: null - when: ansible_version.full is version('2.11.1', '>=', strict=True) + when: + - ansible_version_major_minor_patch is version('2.11.1', '>=', strict=True) - set_fact: expected_ssh_port: 22 - when: ansible_version.full is version('2.11.1', '<', strict=True) + when: + - ansible_version_major_minor_patch is version('2.11.1', '<', strict=True) diff --git a/tests/ansible/integration/action/transfer_data.yml b/tests/ansible/integration/action/transfer_data.yml index ab994683..c36dd463 100644 --- a/tests/ansible/integration/action/transfer_data.yml +++ b/tests/ansible/integration/action/transfer_data.yml @@ -1,7 +1,14 @@ -- name: integration/action/transfer_data.yml +- name: integration/action/transfer_data.yml, json hosts: test-targets tasks: + - meta: end_play + when: + # Ansible >= 12 (ansible-core >= 2.19) only allows bytes|str through + # `ansible.plugins.action.ActionBase._transfer_data()`. + - ansible_version_major_minor is version('2.19', '>=', strict=True) + - not is_mitogen + - name: Cleanup transfer data file: path: /tmp/transfer-data @@ -15,26 +22,41 @@ data: { "I am JSON": true } + - name: Slurp JSON transfer data slurp: src: /tmp/transfer-data register: out + - assert: that: | out.content|b64decode == '{"I am JSON": true}' fail_msg: | out={{ out }} + - name: Cleanup transfer data + file: + path: /tmp/transfer-data + state: absent + tags: + - transfer_data + + +- name: integration/action/transfer_data.yml, text + hosts: test-targets + tasks: - name: Create text transfer data action_passthrough: method: _transfer_data kwargs: remote_path: /tmp/transfer-data data: "I am text." + - name: Slurp text transfer data slurp: src: /tmp/transfer-data register: out + - assert: that: out.content|b64decode == 'I am text.' diff --git a/tests/ansible/integration/async/result_shell_echo_hi.yml b/tests/ansible/integration/async/result_shell_echo_hi.yml index d3f9d42c..6e726fc5 100644 --- a/tests/ansible/integration/async/result_shell_echo_hi.yml +++ b/tests/ansible/integration/async/result_shell_echo_hi.yml @@ -35,8 +35,8 @@ # | Ansible <= 3 | ansible-core <= 2.10 | present | True | # | Ansible 4 - 6 | ansible-core 2.11 - 2.13 | deprecated | False | # | Ansible >= 7 | ansible-core >= 2.14 | absent | n/a | - - (ansible_version.full is version("2.14", ">=", strict=True) and async_out.invocation.module_args.warn is not defined) - or (ansible_version.full is version("2.11", ">=", strict=True) and async_out.invocation.module_args.warn == False) + - (ansible_version_major_minor is version("2.14", ">=", strict=True) and async_out.invocation.module_args.warn is not defined) + or (ansible_version_major_minor is version("2.11", ">=", strict=True) and async_out.invocation.module_args.warn == False) or (async_out.invocation.module_args.warn == True) - async_out.rc == 0 - async_out.start.startswith("20") @@ -53,7 +53,7 @@ fail_msg: | async_out={{ async_out }} when: - - ansible_version.full is version('2.4', '>=', strict=True) + - ansible_version_major_minor is version('2.4', '>=', strict=True) vars: async_out: "{{result.content|b64decode|from_json}}" tags: diff --git a/tests/ansible/integration/async/runner_one_job.yml b/tests/ansible/integration/async/runner_one_job.yml index de7a22e6..db9b126f 100644 --- a/tests/ansible/integration/async/runner_one_job.yml +++ b/tests/ansible/integration/async/runner_one_job.yml @@ -41,9 +41,9 @@ - result1.changed == True # ansible/b72e989e1837ccad8dcdc926c43ccbc4d8cdfe44 - | - (ansible_version.full is version('2.8', ">=", strict=True) and + (ansible_version_major_minor is version('2.8', ">=", strict=True) and result1.cmd == "echo alldone;\nsleep 1;\n") or - (ansible_version.full is version('2.8', '<', strict=True) and + (ansible_version_major_minor is version('2.8', '<', strict=True) and result1.cmd == "echo alldone;\n sleep 1;") - result1.delta|length == 14 - result1.start|length == 26 @@ -61,7 +61,7 @@ fail_msg: | result1={{ result1 }} when: - - ansible_version.full is version('2.8', '>', strict=True) # ansible#51393 + - ansible_version_major_minor is version('2.8', '>', strict=True) # ansible#51393 - assert: that: @@ -69,6 +69,6 @@ fail_msg: | result1={{ result1 }} when: - - ansible_version.full is version('2.4', '>', strict=True) + - ansible_version_major_minor is version('2.4', '>', strict=True) tags: - runner_one_job diff --git a/tests/ansible/integration/async/runner_two_simultaneous_jobs.yml b/tests/ansible/integration/async/runner_two_simultaneous_jobs.yml index 74a50318..8c462ec2 100644 --- a/tests/ansible/integration/async/runner_two_simultaneous_jobs.yml +++ b/tests/ansible/integration/async/runner_two_simultaneous_jobs.yml @@ -66,6 +66,6 @@ fail_msg: | result2={{ result2 }} when: - - ansible_version.full is version('2.8', '>=', strict=True) # ansible#51393 + - ansible_version_major_minor is version('2.8', '>=', strict=True) # ansible#51393 tags: - runner_two_simultaneous_jobs diff --git a/tests/ansible/integration/connection/reset.yml b/tests/ansible/integration/connection/reset.yml index 2d7a75d3..9dea33ee 100644 --- a/tests/ansible/integration/connection/reset.yml +++ b/tests/ansible/integration/connection/reset.yml @@ -10,11 +10,11 @@ - debug: msg="reset.yml skipped on Ansible<2.5.6" when: - - ansible_version.full is version('2.5.6', '<', strict=True) + - ansible_version_major_minor_patch is version('2.5.6', '<', strict=True) - meta: end_play when: - - ansible_version.full is version('2.5.6', '<', strict=True) + - ansible_version_major_minor_patch is version('2.5.6', '<', strict=True) - custom_python_detect_environment: register: out diff --git a/tests/ansible/integration/connection/reset_become.yml b/tests/ansible/integration/connection/reset_become.yml index 2548df17..0e0aefa9 100644 --- a/tests/ansible/integration/connection/reset_become.yml +++ b/tests/ansible/integration/connection/reset_become.yml @@ -8,11 +8,11 @@ tasks: - debug: msg="reset_become.yml skipped on Ansible<2.5.6" when: - - ansible_version.full is version('2.5.6', '<', strict=True) + - ansible_version_major_minor_patch is version('2.5.6', '<', strict=True) - meta: end_play when: - - ansible_version.full is version('2.5.6', '<', strict=True) + - ansible_version_major_minor_patch is version('2.5.6', '<', strict=True) - name: save pid of the become acct custom_python_detect_environment: diff --git a/tests/ansible/integration/connection_delegation/delegate_to_template.yml b/tests/ansible/integration/connection_delegation/delegate_to_template.yml index 60a67b82..8cd50f98 100644 --- a/tests/ansible/integration/connection_delegation/delegate_to_template.yml +++ b/tests/ansible/integration/connection_delegation/delegate_to_template.yml @@ -19,7 +19,7 @@ - meta: end_play when: - - ansible_version.full is version('2.4', '<', strict=True) + - ansible_version_major_minor is version('2.4', '<', strict=True) - mitogen_get_stack: delegate_to: "{{ physical_host }}" @@ -50,7 +50,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'alias-user', }, @@ -78,7 +78,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'ansible-cfg-remote-user', }, diff --git a/tests/ansible/integration/connection_delegation/stack_construction.yml b/tests/ansible/integration/connection_delegation/stack_construction.yml index 1cfd34ef..58abac7b 100644 --- a/tests/ansible/integration/connection_delegation/stack_construction.yml +++ b/tests/ansible/integration/connection_delegation/stack_construction.yml @@ -87,7 +87,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'alias-user', }, @@ -131,7 +131,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'alias-user', }, @@ -186,7 +186,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'ansible-cfg-remote-user', }, @@ -230,7 +230,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'alias-user', }, @@ -258,7 +258,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'ansible-cfg-remote-user', }, @@ -312,7 +312,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'newuser-normal-normal-user', }, @@ -357,7 +357,7 @@ -o, PubkeyAcceptedKeyTypes=+ssh-rsa, -o, UserKnownHostsFile=/dev/null, ], - 'ssh_debug_level': null, + 'ssh_debug_level': 0, 'ssh_path': 'ssh', 'username': 'alias-user', }, diff --git a/tests/ansible/integration/context_service/disconnect_cleanup.yml b/tests/ansible/integration/context_service/disconnect_cleanup.yml index 22ba12eb..a87dae3d 100644 --- a/tests/ansible/integration/context_service/disconnect_cleanup.yml +++ b/tests/ansible/integration/context_service/disconnect_cleanup.yml @@ -8,7 +8,7 @@ - meta: end_play when: - - ansible_version.full is version('2.5.6', '<', strict=True) + - ansible_version_major_minor_patch is version('2.5.6', '<', strict=True) # Start with a clean slate. - mitogen_shutdown_all: diff --git a/tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml b/tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml index eddca199..451b4dc3 100644 --- a/tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml +++ b/tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml @@ -46,9 +46,9 @@ '20': /usr/bin/python3.8 discovered_interpreter_expected: >- - {%- if ansible_version.full is version('2.12', '<', strict=True) -%} + {%- if ansible_version_major_minor is version('2.12', '<', strict=True) -%} {{ DISCOVERED_INTERPRETER_EXPECTED_MAP__ANSIBLE_lt_2_12[distro][distro_major] }} - {%- elif ansible_version.full is version('2.17', '<', strict=True) -%} + {%- elif ansible_version_major_minor is version('2.17', '<', strict=True) -%} {{ DISCOVERED_INTERPRETER_EXPECTED_MAP__ANSIBLE_2_12_to_2_16[distro][distro_major] }} {%- else -%} {{ DISCOVERED_INTERPRETER_EXPECTED_MAP__ANSIBLE_ge_2_17[distro][distro_major] }} @@ -141,7 +141,7 @@ when: - legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python' - auto_out.ansible_facts.discovered_interpreter_python != '/usr/bin/python' - - ansible_version.full is version_compare('2.12.0', '<', strict=True) + - ansible_version_major_minor is version('2.12', '<', strict=True) - name: check for warning (only on platforms where auto result is not /usr/bin/python and legacy is) from ansible 2.12 on assert: @@ -153,7 +153,7 @@ when: - legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python' - auto_out.ansible_facts.discovered_interpreter_python != '/usr/bin/python' - - ansible_version.full is version_compare('2.12.0', '>=', strict=True) + - ansible_version_major_minor is version('2.12', '>=', strict=True) - name: test that auto_silent never warns and got the same answer as auto block: @@ -229,6 +229,6 @@ always: - meta: clear_facts when: - - ansible_version.full is version_compare('2.8.0', '>=', strict=True) + - ansible_version_major_minor is version('2.8', '>=', strict=True) tags: - ansible_2_8_tests diff --git a/tests/ansible/integration/interpreter_discovery/complex_args.yml b/tests/ansible/integration/interpreter_discovery/complex_args.yml index f9770876..555a676b 100644 --- a/tests/ansible/integration/interpreter_discovery/complex_args.yml +++ b/tests/ansible/integration/interpreter_discovery/complex_args.yml @@ -14,7 +14,7 @@ - meta: end_play when: - not is_mitogen - - ansible_version.full is version('2.17.1', '>=', strict=True) + - ansible_version_major_minor_patch is version('2.17.1', '>=', strict=True) - name: create temp file to source file: diff --git a/tests/ansible/integration/process/unix_socket_cleanup.yml b/tests/ansible/integration/process/unix_socket_cleanup.yml index 21747494..4466aa2e 100644 --- a/tests/ansible/integration/process/unix_socket_cleanup.yml +++ b/tests/ansible/integration/process/unix_socket_cleanup.yml @@ -11,6 +11,8 @@ vars: ansible_python_interpreter: "{{ ansible_playbook_python }}" shell: >- + ANSIBLE_CALLBACK_RESULT_FORMAT=json + ANSIBLE_LOAD_CALLBACK_PLUGINS=false ANSIBLE_STRATEGY=mitogen_linear ANSIBLE_SSH_ARGS="-o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa" ANSIBLE_VERBOSITY="{{ ansible_verbosity }}" diff --git a/tests/ansible/integration/runner/crashy_new_style_module.yml b/tests/ansible/integration/runner/crashy_new_style_module.yml index 80833ab8..5a4d6651 100644 --- a/tests/ansible/integration/runner/crashy_new_style_module.yml +++ b/tests/ansible/integration/runner/crashy_new_style_module.yml @@ -18,10 +18,17 @@ - not out.changed - out is failed # https://github.com/ansible/ansible/commit/62d8c8fde6a76d9c567ded381e9b34dad69afcd6 - - out.msg is match(msg_pattern) - - (out.module_stdout == "" and out.module_stderr is search(tb_pattern)) - or - (out.module_stdout is search(tb_pattern) and out.module_stderr is match("Shared connection to localhost closed.")) + - | + out.msg is match(msg_pattern) + or out.msg in ( + "Task failed: Module failed: name 'kaboom' is not defined", + 'Module result deserialization failed: No start of json char found', + ) + # - out.exception is undefined + # or out.exception | default('') is match(tb_pattern) + # or out.module_stderr is search(tb_pattern) + # - out.module_stdout == '' + # - out.module_stderr is search(tb_pattern) fail_msg: | out={{ out }} tags: diff --git a/tests/ansible/integration/runner/custom_bash_hashbang_argument.yml b/tests/ansible/integration/runner/custom_bash_hashbang_argument.yml index 34a60e61..c750317a 100644 --- a/tests/ansible/integration/runner/custom_bash_hashbang_argument.yml +++ b/tests/ansible/integration/runner/custom_bash_hashbang_argument.yml @@ -7,7 +7,7 @@ - meta: end_play when: - not is_mitogen - - ansible_version.full is version('2.17.1', '>=', strict=True) + - ansible_version_major_minor_patch is version('2.17.1', '>=', strict=True) - custom_bash_old_style_module: foo: true diff --git a/tests/ansible/integration/runner/custom_binary_producing_junk.yml b/tests/ansible/integration/runner/custom_binary_producing_junk.yml index 2a05fb75..c8ab869a 100644 --- a/tests/ansible/integration/runner/custom_binary_producing_junk.yml +++ b/tests/ansible/integration/runner/custom_binary_producing_junk.yml @@ -30,6 +30,7 @@ - out.failed - out.results[0].failed - out.results[0].msg.startswith('MODULE FAILURE') + or out.results[0].msg.startswith('Module result deserialization failed') - out.results[0].rc == 0 fail_msg: | out={{ out }} diff --git a/tests/ansible/integration/runner/custom_binary_single_null.yml b/tests/ansible/integration/runner/custom_binary_single_null.yml index cfd401f8..bb5ec5d0 100644 --- a/tests/ansible/integration/runner/custom_binary_single_null.yml +++ b/tests/ansible/integration/runner/custom_binary_single_null.yml @@ -15,7 +15,9 @@ that: - "out.failed" - "out.results[0].failed" - - "out.results[0].msg.startswith('MODULE FAILURE')" + - | + out.results[0].msg.startswith('MODULE FAILURE') + or out.results[0].msg == 'Module result deserialization failed: No start of json char found' # On Ubuntu 16.04 /bin/sh is dash 0.5.8. It treats custom_binary_single_null # as a valid executable. There's no error message, and rc == 0. - | diff --git a/tests/ansible/integration/runner/custom_perl_json_args_module.yml b/tests/ansible/integration/runner/custom_perl_json_args_module.yml index a34b6b75..c5c647cd 100644 --- a/tests/ansible/integration/runner/custom_perl_json_args_module.yml +++ b/tests/ansible/integration/runner/custom_perl_json_args_module.yml @@ -20,6 +20,6 @@ fail_msg: | out={{ out }} when: - - ansible_version.full is version('2.4', '>=', strict=True) + - ansible_version_major_minor is version('2.4', '>=', strict=True) tags: - custom_perl_json_args_module diff --git a/tests/ansible/integration/runner/custom_perl_want_json_module.yml b/tests/ansible/integration/runner/custom_perl_want_json_module.yml index 28ad7f7f..91ec5672 100644 --- a/tests/ansible/integration/runner/custom_perl_want_json_module.yml +++ b/tests/ansible/integration/runner/custom_perl_want_json_module.yml @@ -20,6 +20,6 @@ fail_msg: | out={{ out }} when: - - ansible_version.full is version('2.4', '>=', strict=True) + - ansible_version_major_minor is version('2.4', '>=', strict=True) tags: - custom_perl_want_json_module diff --git a/tests/ansible/integration/runner/missing_module.yml b/tests/ansible/integration/runner/missing_module.yml index 4d3f6823..b641cbe3 100644 --- a/tests/ansible/integration/runner/missing_module.yml +++ b/tests/ansible/integration/runner/missing_module.yml @@ -5,6 +5,8 @@ - name: Run missing_module connection: local environment: + ANSIBLE_CALLBACK_RESULT_FORMAT: json + ANSIBLE_LOAD_CALLBACK_PLUGINS: "false" ANSIBLE_STRATEGY: "{{ lookup('env', 'ANSIBLE_STRATEGY') | mandatory }}" ANSIBLE_VERBOSITY: "{{ ansible_verbosity }}" vars: @@ -26,6 +28,7 @@ - assert: that: | 'The module missing_module was not found in configured module paths' in out.stdout + or "Cannot resolve 'missing_module' to an action or module" in out.stdout fail_msg: | out={{ out }} tags: diff --git a/tests/ansible/integration/ssh/all.yml b/tests/ansible/integration/ssh/all.yml index 20031704..6dbf945f 100644 --- a/tests/ansible/integration/ssh/all.yml +++ b/tests/ansible/integration/ssh/all.yml @@ -8,3 +8,4 @@ - import_playbook: templated_by_play_taskvar.yml - import_playbook: templated_by_task_keyword.yml - import_playbook: variables.yml +- import_playbook: verbosity.yml diff --git a/tests/ansible/integration/ssh/password.yml b/tests/ansible/integration/ssh/password.yml index 21ab6f15..ca08fa5b 100644 --- a/tests/ansible/integration/ssh/password.yml +++ b/tests/ansible/integration/ssh/password.yml @@ -31,6 +31,11 @@ - assert: that: - ssh_no_password_result.unreachable == True + - >- + ssh_no_password_result.msg is search('SSH password was requested, but none specified') + or ssh_no_password_result.msg is search('SSH password is incorrect') + or ssh_no_password_result.msg is search('Invalid/incorrect password') + or ssh_no_password_result.msg is search('Permission denied \(publickey,password(,keyboard-interactive)?\)') fail_msg: | ssh_no_password_result={{ ssh_no_password_result }} @@ -64,5 +69,9 @@ - assert: that: - ssh_wrong_password_result.unreachable == True + - >- + ssh_wrong_password_result.msg is search('SSH password is incorrect') + or ssh_wrong_password_result.msg is search('Invalid/incorrect password') + or ssh_wrong_password_result.msg is search('Permission denied \(publickey,password(,keyboard-interactive)?\)') fail_msg: | ssh_wrong_password_result={{ ssh_wrong_password_result }} diff --git a/tests/ansible/integration/ssh/templated_by_play_taskvar.yml b/tests/ansible/integration/ssh/templated_by_play_taskvar.yml index c5c2e544..714d05dc 100644 --- a/tests/ansible/integration/ssh/templated_by_play_taskvar.yml +++ b/tests/ansible/integration/ssh/templated_by_play_taskvar.yml @@ -32,7 +32,7 @@ when: # https://github.com/ansible/ansible/issues/84238 - not is_mitogen - - ansible_version.full is version('2.19', '<', strict=True) + - ansible_version_major_minor is version('2.19', '<', strict=True) - meta: reset_connection - name: Templated variables in play, key authentication ping: diff --git a/tests/ansible/integration/ssh/timeouts.yml b/tests/ansible/integration/ssh/timeouts.yml index afc5e5a2..0cb59422 100644 --- a/tests/ansible/integration/ssh/timeouts.yml +++ b/tests/ansible/integration/ssh/timeouts.yml @@ -13,6 +13,8 @@ - name: Cause Ansible connection timeout connection: local environment: + ANSIBLE_CALLBACK_RESULT_FORMAT: json + ANSIBLE_LOAD_CALLBACK_PLUGINS: "false" ANSIBLE_SSH_TIMEOUT: 10 ANSIBLE_STRATEGY: "{{ lookup('env', 'ANSIBLE_STRATEGY') | mandatory }}" ANSIBLE_VERBOSITY: "{{ ansible_verbosity }}" @@ -43,6 +45,7 @@ '"unreachable": true' in out.stdout - | '"msg": "Connection timed out."' in out.stdout + or '"msg": "Task failed: Connection timed out."' in out.stdout fail_msg: | out={{ out }} tags: diff --git a/tests/ansible/integration/ssh/variables.yml b/tests/ansible/integration/ssh/variables.yml index bb4bd179..5eb54dde 100644 --- a/tests/ansible/integration/ssh/variables.yml +++ b/tests/ansible/integration/ssh/variables.yml @@ -19,6 +19,8 @@ - name: ansible_user, ansible_ssh_private_key_file shell: > ANSIBLE_ANY_ERRORS_FATAL=false + ANSIBLE_CALLBACK_RESULT_FORMAT=json + ANSIBLE_LOAD_CALLBACK_PLUGINS=false ANSIBLE_STRATEGY=mitogen_linear ANSIBLE_SSH_ARGS="-o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa" ANSIBLE_VERBOSITY="{{ ansible_verbosity }}" @@ -37,6 +39,8 @@ - name: ansible_user, wrong ansible_ssh_private_key_file shell: > ANSIBLE_ANY_ERRORS_FATAL=false + ANSIBLE_CALLBACK_RESULT_FORMAT=json + ANSIBLE_LOAD_CALLBACK_PLUGINS=false ANSIBLE_STRATEGY=mitogen_linear ANSIBLE_SSH_ARGS="-o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa" ANSIBLE_VERBOSITY="{{ ansible_verbosity }}" diff --git a/tests/ansible/integration/ssh/verbosity.yml b/tests/ansible/integration/ssh/verbosity.yml new file mode 100644 index 00000000..2bf1ed10 --- /dev/null +++ b/tests/ansible/integration/ssh/verbosity.yml @@ -0,0 +1,41 @@ +# Ansible >= 12 (ansible-core >= 2.19) adds SSH connection `verbosity` property. +# Ansible <= 11 (ansible-core <= 2.18) applies ANSIBLE_VERBOSITY -> ssh. + +- name: integration/ssh/verbosity.yml + hosts: test-targets + gather_facts: false + tasks: + - meta: end_play + when: + - ansible_version_major_minor is version('2.19', '<', strict=True) + + - name: Exercise ssh verbosity + connection: local + environment: + ANSIBLE_CALLBACK_RESULT_FORMAT: json + ANSIBLE_LOAD_CALLBACK_PLUGINS: "false" + ANSIBLE_SSH_VERBOSITY: 3 + ANSIBLE_STRATEGY: "{{ lookup('env', 'ANSIBLE_STRATEGY') | mandatory }}" + ANSIBLE_VERBOSITY: 3 + vars: + ansible_python_interpreter: "{{ ansible_playbook_python }}" + command: + cmd: + ansible + {% for inv in ansible_inventory_sources %} + -i "{{ inv }}" + {% endfor %} + "{{ inventory_hostname }}" + -m ping + chdir: ../.. + register: out + changed_when: false + check_mode: false + + - name: Verify ssh -vvv output is included + assert: + that: + - >- + out.stdout is search('debug1: Reading configuration data') + fail_msg: | + out={{ out }} diff --git a/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml b/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml index a53f75ed..3a4f4e6e 100644 --- a/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml +++ b/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml @@ -9,7 +9,7 @@ - command: sudo -n whoami args: - warn: "{{ False if ansible_version.full is version('2.10', '<=', strict=True) else omit }}" + warn: "{{ False if ansible_version_major_minor is version('2.10', '<=', strict=True) else omit }}" ignore_errors: true register: sudo_available diff --git a/tests/ansible/integration/stub_connections/kubectl.yml b/tests/ansible/integration/stub_connections/kubectl.yml index 8fe061d1..c9486d90 100644 --- a/tests/ansible/integration/stub_connections/kubectl.yml +++ b/tests/ansible/integration/stub_connections/kubectl.yml @@ -7,7 +7,7 @@ - meta: end_play when: - - ansible_version.full is version('2.5', '<', strict=True) + - ansible_version_major_minor is version('2.5', '<', strict=True) - custom_python_detect_environment: vars: diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml index 584a6806..dc7340a8 100644 --- a/tests/ansible/integration/stub_connections/setns_lxc.yml +++ b/tests/ansible/integration/stub_connections/setns_lxc.yml @@ -15,6 +15,8 @@ - name: Run stub-lxc-info.py environment: + ANSIBLE_CALLBACK_RESULT_FORMAT: json + ANSIBLE_LOAD_CALLBACK_PLUGINS: "false" ANSIBLE_STRATEGY: "{{ lookup('env', 'ANSIBLE_STRATEGY') | mandatory }}" ANSIBLE_VERBOSITY: "{{ ansible_verbosity }}" command: | @@ -30,7 +32,7 @@ localhost args: chdir: ../.. - warn: "{{ False if ansible_version.full is version('2.10', '<=', strict=True) else omit }}" + warn: "{{ False if ansible_version_major_minor is version('2.10', '<=', strict=True) else omit }}" register: result - assert: diff --git a/tests/ansible/integration/stub_connections/setns_lxd.yml b/tests/ansible/integration/stub_connections/setns_lxd.yml index 2e07aca3..127941f0 100644 --- a/tests/ansible/integration/stub_connections/setns_lxd.yml +++ b/tests/ansible/integration/stub_connections/setns_lxd.yml @@ -15,6 +15,8 @@ - name: Run ansible stub-lxc.py environment: + ANSIBLE_CALLBACK_RESULT_FORMAT: json + ANSIBLE_LOAD_CALLBACK_PLUGINS: "false" ANSIBLE_STRATEGY: "{{ lookup('env', 'ANSIBLE_STRATEGY') | mandatory }}" ANSIBLE_VERBOSITY: "{{ ansible_verbosity }}" command: | @@ -30,7 +32,7 @@ localhost args: chdir: ../.. - warn: "{{ False if ansible_version.full is version('2.10', '<=', strict=True) else omit }}" + warn: "{{ False if ansible_version_major_minor is version('2.10', '<=', strict=True) else omit }}" register: result - assert: diff --git a/tests/ansible/integration/transport_config/password.yml b/tests/ansible/integration/transport_config/password.yml index 5a1968e0..34ed1a41 100644 --- a/tests/ansible/integration/transport_config/password.yml +++ b/tests/ansible/integration/transport_config/password.yml @@ -8,9 +8,16 @@ tasks: - include_tasks: ../_mitogen_only.yml - {mitogen_get_stack: {}, register: out} + - assert_equal: + left: out.result[0].kwargs.password + right: null + when: + - ansible_version_major_minor is version('2.19', '>=', strict=True) - assert_equal: left: out.result[0].kwargs.password right: "" # actually null, but assert_equal limitation + when: + - ansible_version_major_minor is version('2.19', '<', strict=True) tags: - mitogen_only @@ -23,9 +30,16 @@ - assert_equal: left: out.result[0].kwargs.password right: "ansi-ssh-pass" + - assert_equal: + left: out.result[1].kwargs.password + right: null + when: + - ansible_version_major_minor is version('2.19', '>=', strict=True) - assert_equal: left: out.result[1].kwargs.password right: "" + when: + - ansible_version_major_minor is version('2.19', '<', strict=True) tags: - mitogen_only @@ -48,9 +62,16 @@ tasks: - include_tasks: ../_mitogen_only.yml - {mitogen_get_stack: {}, register: out} + - assert_equal: + left: out.result[0].kwargs.password + right: null + when: + - ansible_version_major_minor is version('2.19', '>=', strict=True) - assert_equal: left: out.result[0].kwargs.password right: "" + when: + - ansible_version_major_minor is version('2.19', '<', strict=True) - assert_equal: left: out.result[1].kwargs.password right: "ansi-ssh-pass" @@ -76,9 +97,16 @@ tasks: - include_tasks: ../_mitogen_only.yml - {mitogen_get_stack: {}, register: out} + - assert_equal: + left: out.result[0].kwargs.password + right: null + when: + - ansible_version_major_minor is version('2.19', '>=', strict=True) - assert_equal: left: out.result[0].kwargs.password right: "" + when: + - ansible_version_major_minor is version('2.19', '<', strict=True) - assert_equal: left: out.result[1].kwargs.password right: "ansi-pass" @@ -104,9 +132,16 @@ tasks: - include_tasks: ../_mitogen_only.yml - {mitogen_get_stack: {}, register: out} + - assert_equal: + left: out.result[0].kwargs.password + right: null + when: + - ansible_version_major_minor is version('2.19', '>=', strict=True) - assert_equal: left: out.result[0].kwargs.password right: "" + when: + - ansible_version_major_minor is version('2.19', '<', strict=True) - assert_equal: left: out.result[1].kwargs.password right: "c.b.a" diff --git a/tests/ansible/integration/transport_config/python_path.yml b/tests/ansible/integration/transport_config/python_path.yml index 21f3928c..b8bb286b 100644 --- a/tests/ansible/integration/transport_config/python_path.yml +++ b/tests/ansible/integration/transport_config/python_path.yml @@ -12,7 +12,7 @@ - include_tasks: ../_mitogen_only.yml - meta: end_play when: - - ansible_version.full is version('2.17', '>=', strict=True) + - ansible_version_major_minor is version('2.17', '>=', strict=True) - {mitogen_get_stack: {}, register: out} - assert_equal: left: out.result[0].kwargs.python_path @@ -27,7 +27,7 @@ - include_tasks: ../_mitogen_only.yml - meta: end_play when: - - ansible_version.full is version('2.17', '>=', strict=True) + - ansible_version_major_minor is version('2.17', '>=', strict=True) - {mitogen_get_stack: {}, register: out} - assert_equal: left: out.result[0].kwargs.python_path @@ -57,7 +57,7 @@ - include_tasks: ../_mitogen_only.yml - meta: end_play when: - - ansible_version.full is version('2.17', '>=', strict=True) + - ansible_version_major_minor is version('2.17', '>=', strict=True) - {mitogen_get_stack: {}, register: out} - assert_equal: left: out.result[0].kwargs.python_path @@ -87,7 +87,7 @@ - include_tasks: ../_mitogen_only.yml - meta: end_play when: - - ansible_version.full is version('2.17', '>=', strict=True) + - ansible_version_major_minor is version('2.17', '>=', strict=True) - {mitogen_get_stack: {}, register: out} - assert_equal: left: out.result[0].kwargs.python_path diff --git a/tests/ansible/regression/issue_1066__add_host__host_key_checking.yml b/tests/ansible/regression/issue_1066__add_host__host_key_checking.yml index dd754f84..cdc50fc3 100644 --- a/tests/ansible/regression/issue_1066__add_host__host_key_checking.yml +++ b/tests/ansible/regression/issue_1066__add_host__host_key_checking.yml @@ -31,7 +31,7 @@ # FIXME https://github.com/mitogen-hq/mitogen/issues/1096 - meta: end_play when: - - ansible_version.full is version('2.17', '>=', strict=True) + - ansible_version_major_minor is version('2.17', '>=', strict=True) - meta: reset_connection # The host key might be in ~/.ssh/known_hosts. If it's removed then no @@ -65,7 +65,7 @@ # ansible_host_key_checking don't work on Vanilla Ansible 2.10, even for # static inventory hosts (ansible/ansible#49254, ansible/ansible#73708). when: - - ansible_version.full is version('2.11', '>=', strict=True) + - ansible_version_major_minor is version('2.11', '>=', strict=True) or is_mitogen tags: - issue_1066 diff --git a/tests/ansible/regression/issue_109__target_has_old_ansible_installed.yml b/tests/ansible/regression/issue_109__target_has_old_ansible_installed.yml index a7ae0908..550a7f4c 100644 --- a/tests/ansible/regression/issue_109__target_has_old_ansible_installed.yml +++ b/tests/ansible/regression/issue_109__target_has_old_ansible_installed.yml @@ -7,7 +7,7 @@ tasks: - meta: end_play when: - - ansible_version.full is version('2.6', '<', strict=True) + - ansible_version_major_minor is version('2.6', '<', strict=True) - name: Copy the naughty ansible into place copy: @@ -24,7 +24,8 @@ assert: that: - env.cwd == ansible_user_dir - - (not env.mitogen_loaded) or (env.python_path.count("") == 1) + - not env.mitogen_loaded + or (env.python_path | select('eq', '') | length == 1) fail_msg: | ansible_user_dir={{ ansible_user_dir }} env={{ env }} diff --git a/tests/ansible/regression/issue_590__sys_modules_crap.yml b/tests/ansible/regression/issue_590__sys_modules_crap.yml index 2c2262f4..f2bda94c 100644 --- a/tests/ansible/regression/issue_590__sys_modules_crap.yml +++ b/tests/ansible/regression/issue_590__sys_modules_crap.yml @@ -3,7 +3,7 @@ tasks: - meta: end_play when: - - ansible_version.full is version('2.8', '<', strict=True) + - ansible_version_major_minor is version('2.8', '<', strict=True) - custom_python_uses_distro: register: out diff --git a/tests/ansible/regression/issue_655__wait_for_connection_error.yml b/tests/ansible/regression/issue_655__wait_for_connection_error.yml index 506d5516..297b6043 100644 --- a/tests/ansible/regression/issue_655__wait_for_connection_error.yml +++ b/tests/ansible/regression/issue_655__wait_for_connection_error.yml @@ -22,7 +22,7 @@ when: # Ansible 10 (ansible-core 2.17+) require Python 3.7+ on targets. # On CentOS 8 /usr/libexec/platform-python is Python 3.6 - - ansible_version.full is version('2.17', '>=', strict=True) + - ansible_version_major_minor is version('2.17', '>=', strict=True) - name: set up test container and run tests inside it block: diff --git a/tests/ansible/regression/issue_766__get_with_context.yml b/tests/ansible/regression/issue_766__get_with_context.yml index 38e33275..5dae142f 100644 --- a/tests/ansible/regression/issue_766__get_with_context.yml +++ b/tests/ansible/regression/issue_766__get_with_context.yml @@ -25,8 +25,15 @@ # with Ansible 4 (ansible-core 2.11) & associated collections. # ansible.module_utils.connection.ConnectionError: Method not found # https://github.com/mitogen-hq/mitogen/actions/runs/12854359099/job/35838635886 - - ansible_version.full is version('2.11', '>=', strict=True) - - ansible_version.full is version('2.12', '<', strict=True) + - ansible_version_major_minor is version('2.11', '>=', strict=True) + - ansible_version_major_minor is version('2.12', '<', strict=True) + + - meta: end_play + when: + # TASK [Get running configuration and state data ] + # Error: : Task failed: ActionBase._parse_returned_data() missing 1 required positional argument: 'profile' + # https://github.com/ansible-collections/ansible.netcommon/issues/698#issuecomment-2910082548 + - ansible_version_major_minor is version('2.19', '>=', strict=True) - block: - name: Start container diff --git a/tests/ansible/regression/issue_776__load_plugins_called_twice.yml b/tests/ansible/regression/issue_776__load_plugins_called_twice.yml index d482c41c..4a4cf7be 100755 --- a/tests/ansible/regression/issue_776__load_plugins_called_twice.yml +++ b/tests/ansible/regression/issue_776__load_plugins_called_twice.yml @@ -17,7 +17,7 @@ # support Python 2.x on targets. - meta: end_play when: - - ansible_version.full is version('2.17', '>=', strict=True) + - ansible_version_major_minor is version('2.17', '>=', strict=True) roles: - role: package_manager diff --git a/tests/ansible/tests/utils_unsafe_test.py b/tests/ansible/tests/utils_unsafe_test.py index 9aa461c5..2ca863b7 100644 --- a/tests/ansible/tests/utils_unsafe_test.py +++ b/tests/ansible/tests/utils_unsafe_test.py @@ -4,6 +4,7 @@ from ansible.utils.unsafe_proxy import AnsibleUnsafeBytes from ansible.utils.unsafe_proxy import AnsibleUnsafeText from ansible.utils.unsafe_proxy import wrap_var +import ansible_mitogen.utils import ansible_mitogen.utils.unsafe import mitogen.core @@ -17,7 +18,7 @@ class Text(mitogen.core.UnicodeType): pass class Tuple(tuple): pass -class CastTest(unittest.TestCase): +class CastMixin(unittest.TestCase): def assertIsType(self, obj, cls, msg=None): self.assertIs(type(obj), cls, msg) @@ -29,6 +30,8 @@ class CastTest(unittest.TestCase): self.assertEqual(cast(obj), expected) self.assertIsType(cast(obj), type(expected)) + +class CastKnownTest(CastMixin): def test_ansible_unsafe(self): self.assertCasts(AnsibleUnsafeBytes(b'abc'), b'abc') self.assertCasts(AnsibleUnsafeText(u'abc'), u'abc') @@ -47,14 +50,12 @@ class CastTest(unittest.TestCase): self.assertCasts(wrap_var({}), {}) self.assertCasts(wrap_var([]), []) self.assertCasts(wrap_var(u''), u'') - self.assertCasts(wrap_var(()), []) def test_subtypes_roundtrip(self): self.assertCasts(wrap_var(Bytes()), b'') self.assertCasts(wrap_var(Dict()), {}) self.assertCasts(wrap_var(List()), []) self.assertCasts(wrap_var(Text()), u'') - self.assertCasts(wrap_var(Tuple()), []) def test_subtype_nested_dict(self): obj = Dict(foo=Dict(bar=u'abc')) @@ -75,18 +76,59 @@ class CastTest(unittest.TestCase): self.assertIsType(unwrapped[0], list) self.assertIsType(unwrapped[0][0], mitogen.core.UnicodeType) - def test_subtype_roundtrip_tuple(self): - # wrap_var() preserves sequence types, cast() does not (for now) + +@unittest.skipIf( + ansible_mitogen.utils.ansible_version[:2] <= (2, 18), + 'Ansible <= 11 (ansible-core >= 2.18) does not send/receive sets', +) +class CastSetTest(CastMixin): + def test_set(self): + self.assertCasts(wrap_var(set()), set()) + + def test_set_subclass(self): + self.assertCasts(wrap_var(Set()), set()) + + +class CastTupleTest(CastMixin): + def test_tuple(self): + if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + expected = () + else: + expected = [] + self.assertCasts(wrap_var(Tuple()), expected) + + def test_tuple_subclass(self): + if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + expected = () + else: + expected = [] + self.assertCasts(wrap_var(()), expected) + + def test_tuple_subclass_with_contents(self): + if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + expected = ((u'abc',),) + else: + expected = [[u'abc']] + obj = Tuple([Tuple([u'abc'])]) wrapped = wrap_var(obj) unwrapped = ansible_mitogen.utils.unsafe.cast(wrapped) - self.assertEqual(unwrapped, [[u'abc']]) - self.assertIsType(unwrapped, list) - self.assertIsType(unwrapped[0], list) + self.assertEqual(unwrapped, expected) + self.assertIsType(unwrapped, type(expected)) + self.assertIsType(unwrapped[0], type(expected[0])) self.assertIsType(unwrapped[0][0], mitogen.core.UnicodeType) - def test_unknown_types_raise(self): + +class CastUknownTypeTest(unittest.TestCase): + @unittest.skipIf( + ansible_mitogen.utils.ansible_version[:2] >= (2, 19), + 'Ansible >= 12 (ansible-core >= 2.19) uses/preserves sets', + ) + def test_set_raises(self): cast = ansible_mitogen.utils.unsafe.cast self.assertRaises(TypeError, cast, set()) self.assertRaises(TypeError, cast, Set()) + + def test_complex_raises(self): + cast = ansible_mitogen.utils.unsafe.cast self.assertRaises(TypeError, cast, 4j) diff --git a/tests/image_prep/_container_setup.yml b/tests/image_prep/_container_setup.yml index 4aa3b46d..b95d67a9 100644 --- a/tests/image_prep/_container_setup.yml +++ b/tests/image_prep/_container_setup.yml @@ -66,7 +66,7 @@ dnf: dnf clean all command: "{{ clean_command[ansible_pkg_mgr] }}" args: - warn: "{{ False if ansible_version.full is version('2.10', '<=', strict=True) else omit }}" + warn: "{{ False if ansible_version_major_minor is version('2.10', '<=', strict=True) else omit }}" - name: Clean up apt package lists shell: rm -rf {{item}}/* diff --git a/tests/image_prep/group_vars/all.yml b/tests/image_prep/group_vars/all.yml index 91ff934d..6545e432 100644 --- a/tests/image_prep/group_vars/all.yml +++ b/tests/image_prep/group_vars/all.yml @@ -1,3 +1,5 @@ +ansible_version_major_minor: "{{ ansible_version.major }}.{{ ansible_version.minor }}" + common_packages: - openssh-server - rsync diff --git a/tests/image_prep/roles/package_manager/files/debian-archive-bullseye-security-automatic.asc b/tests/image_prep/roles/package_manager/files/debian-archive-bullseye-security-automatic.asc new file mode 100644 index 00000000..e09eef42 --- /dev/null +++ b/tests/image_prep/roles/package_manager/files/debian-archive-bullseye-security-automatic.asc @@ -0,0 +1,186 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +TT9yj35WJXkuIZ7GV189g3gogTpu +=Xxba +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tox.ini b/tox.ini index ddb8c88d..569d9936 100644 --- a/tox.ini +++ b/tox.ini @@ -47,16 +47,17 @@ # ansible == 9.x ansible-core ~= 2.16.0 # ansible == 10.x ansible-core ~= 2.17.0 # ansible == 11.x ansible-core ~= 2.18.0 +# ansible == 12.x ansible-core ~= 2.19.0 # See also -# - https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix +# - https://docs.ansible.com/ansible/devel/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix [tox] envlist = init, py{27,36}-mode_ansible-ansible{2.10,3,4}, py{311}-mode_ansible-ansible{2.10,3,4,5}, - py{313}-mode_ansible-ansible{6,7,8,9,10,11}, + py{313}-mode_ansible-ansible{6,7,8,9,10,11,12}, py{27,36,313}-mode_mitogen, report, @@ -80,12 +81,14 @@ deps = ansible3: ansible~=3.0 ansible4: ansible~=4.0 ansible5: ansible~=5.0 + # From Ansible 6 PyPI distributions include a wheel ansible6: ansible~=6.0 ansible7: ansible~=7.0 ansible8: ansible~=8.0 ansible9: ansible~=9.0 ansible10: ansible~=10.0 - ansible11: ansible>=11.0 + ansible11: ansible~=11.0 + ansible12: ansible>=12.0.0b2 install_command = python -m pip --no-python-version-warning --disable-pip-version-check install {opts} {packages} commands_pre = @@ -106,6 +109,9 @@ setenv = NOCOVERAGE_ERASE = 1 NOCOVERAGE_REPORT = 1 PIP_CONSTRAINT={toxinidir}/tests/constraints.txt + # Superceded in Ansible >= 6 (ansible-core >= 2.13) by result_format=yaml + # Deprecated in Ansible 12 (ansible-core 2.19) + ansible{2.10,3-5}: DEFAULT_STDOUT_CALLBACK=yaml # Print warning on the first occurence at each module:linenno in Mitogen. Available Python 2.7, 3.2+. PYTHONWARNINGS=default:::ansible_mitogen,default:::mitogen # Ansible 6 - 8 (ansible-core 2.13 - 2.15) require Python 2.7 or >= 3.5 on targets @@ -118,6 +124,7 @@ setenv = ansible10: MITOGEN_TEST_DISTRO_SPECS=debian10-py3 debian11-py3 ubuntu2004-py3 # Ansible 11 (ansible-core 2.18) requires Python >= 3.8 on targets ansible11: MITOGEN_TEST_DISTRO_SPECS=debian11-py3 ubuntu2004-py3 + ansible12: MITOGEN_TEST_DISTRO_SPECS=debian11-py3 ubuntu2004-py3 distros_centos: MITOGEN_TEST_DISTRO_SPECS=centos6 centos7 centos8 distros_centos5: MITOGEN_TEST_DISTRO_SPECS=centos5 distros_centos6: MITOGEN_TEST_DISTRO_SPECS=centos6