mirror of
https://github.com/ansible/ansible
synced 2026-06-19 07:35:52 +00:00
Drop Python 3.12 controller support (#87057)
* Drop Python 3.12 controller support * Remove obsolete code * Remove obsolete centos6 test code * Skip tests on unsupported platforms * Work around lack of Alpine controller support in tests
This commit is contained in:
@@ -99,8 +99,10 @@ stages:
|
||||
test: rhel/9.7@3.9
|
||||
- name: RHEL 9.7 py312
|
||||
test: rhel/9.7@3.12
|
||||
- name: RHEL 10.1
|
||||
test: rhel/10.1
|
||||
- name: RHEL 10.1 py312
|
||||
test: rhel/10.1@3.12
|
||||
- name: RHEL 10.1 py314
|
||||
test: rhel/10.1@3.14
|
||||
- name: FreeBSD 14.4
|
||||
test: freebsd/14.4
|
||||
- name: FreeBSD 15.0
|
||||
@@ -119,8 +121,6 @@ stages:
|
||||
test: rhel/10.1
|
||||
- name: FreeBSD 14.4
|
||||
test: freebsd/14.4
|
||||
- name: FreeBSD 15.0
|
||||
test: freebsd/15.0
|
||||
groups:
|
||||
- 3
|
||||
- 4
|
||||
@@ -128,16 +128,10 @@ stages:
|
||||
- template: templates/matrix.yml # context/controller (ansible-test container management)
|
||||
parameters:
|
||||
targets:
|
||||
- name: Alpine 3.23
|
||||
test: alpine/3.23
|
||||
- name: Fedora 44
|
||||
test: fedora/44
|
||||
- name: RHEL 9.7
|
||||
test: rhel/9.7
|
||||
- name: RHEL 10.1
|
||||
test: rhel/10.1
|
||||
- name: Ubuntu 24.04
|
||||
test: ubuntu/24.04
|
||||
- name: Ubuntu 26.04
|
||||
test: ubuntu/26.04
|
||||
groups:
|
||||
@@ -174,8 +168,6 @@ stages:
|
||||
parameters:
|
||||
testFormat: linux/{0}
|
||||
targets:
|
||||
- name: Alpine 3.23
|
||||
test: alpine323
|
||||
- name: Fedora 44
|
||||
test: fedora44
|
||||
- name: Ubuntu 24.04
|
||||
@@ -211,7 +203,6 @@ stages:
|
||||
nameFormat: Python {0}
|
||||
testFormat: galaxy/{0}/1
|
||||
targets:
|
||||
- test: 3.12
|
||||
- test: 3.13
|
||||
- test: 3.14
|
||||
- test: 3.15
|
||||
@@ -223,7 +214,6 @@ stages:
|
||||
nameFormat: Python {0}
|
||||
testFormat: generic/{0}/1
|
||||
targets:
|
||||
- test: 3.12
|
||||
- test: 3.13
|
||||
- test: 3.14
|
||||
- test: 3.15
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
major_changes:
|
||||
- ansible - Add support for Python 3.15.
|
||||
- ansible - Drop support for Python 3.12 on the controller.
|
||||
|
||||
@@ -23,7 +23,7 @@ if 1 <= len(sys.argv) <= 2 and os.path.basename(sys.argv[0]) == "ansible" and os
|
||||
|
||||
# Used for determining if the system is running a new enough python version
|
||||
# and should only restrict on our documented minimum versions
|
||||
_PY_MIN = (3, 12)
|
||||
_PY_MIN = (3, 13)
|
||||
|
||||
if sys.version_info < _PY_MIN:
|
||||
raise SystemExit(
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import multiprocessing.resource_tracker
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -27,19 +26,10 @@ def handle_prompt(prompt: str) -> bool:
|
||||
sys.stdout.flush()
|
||||
return True
|
||||
|
||||
# deprecated: description='Python 3.13 and later support track' python_version='3.12'
|
||||
can_track = sys.version_info[:2] >= (3, 13)
|
||||
kwargs = dict(track=False) if can_track else {}
|
||||
|
||||
# This SharedMemory instance is intentionally not closed or unlinked.
|
||||
# Closing will occur naturally in the SharedMemory finalizer.
|
||||
# Unlinking is the responsibility of the process which created it.
|
||||
shm = SharedMemory(name=os.environ['_ANSIBLE_SSH_ASKPASS_SHM'], **kwargs)
|
||||
|
||||
if not can_track:
|
||||
# When track=False is not available, we must unregister explicitly, since it otherwise only occurs during unlink.
|
||||
# This avoids resource tracker noise on stderr during process exit.
|
||||
multiprocessing.resource_tracker.unregister(shm._name, 'shared_memory')
|
||||
shm = SharedMemory(name=os.environ['_ANSIBLE_SSH_ASKPASS_SHM'], track=False)
|
||||
|
||||
cfg = json.loads(shm.buf.tobytes().rstrip(b'\x00'))
|
||||
|
||||
|
||||
@@ -1672,6 +1672,7 @@ INTERPRETER_PYTHON:
|
||||
INTERPRETER_PYTHON_FALLBACK:
|
||||
name: Ordered list of Python interpreters to check for in discovery
|
||||
default:
|
||||
- python3.15
|
||||
- python3.14
|
||||
- python3.13
|
||||
- python3.12
|
||||
|
||||
@@ -67,8 +67,7 @@ class Connection(ConnectionBase):
|
||||
self.cwd = None
|
||||
try:
|
||||
self.default_user = getpass.getuser()
|
||||
except (ImportError, KeyError, OSError):
|
||||
# deprecated: description='only OSError is required for Python 3.13+' python_version='3.12'
|
||||
except OSError:
|
||||
display.vv("Current user (uid=%s) does not seem to exist on this system, leaving user empty." % os.getuid())
|
||||
self.default_user = ""
|
||||
|
||||
|
||||
@@ -472,8 +472,6 @@ b_NOT_SSH_ERRORS = (b'Traceback (most recent call last):', # Python-2.6 when th
|
||||
SSHPASS_AVAILABLE = None
|
||||
SSH_DEBUG = re.compile(r'^debug\d+: .*')
|
||||
|
||||
_HAS_RESOURCE_TRACK = sys.version_info[:2] >= (3, 13)
|
||||
|
||||
PKCS11_DEFAULT_PROMPT = 'Enter PIN for '
|
||||
SSH_ASKPASS_DEFAULT_PROMPT = 'assword'
|
||||
|
||||
@@ -632,11 +630,6 @@ def _clean_resources(func):
|
||||
self.shm.close()
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
self.shm.unlink()
|
||||
if not _HAS_RESOURCE_TRACK:
|
||||
# deprecated: description='unneeded due to track argument for SharedMemory' python_version='3.12'
|
||||
# There is a resource tracking issue where the resource is deleted, but tracking still has a record
|
||||
# This will effectively overwrite the record and remove it
|
||||
SharedMemory(name=self.shm.name, create=True, size=1).unlink()
|
||||
return ret
|
||||
return inner
|
||||
|
||||
@@ -1038,11 +1031,7 @@ class Connection(ConnectionBase):
|
||||
if not conn_password:
|
||||
return popen_kwargs
|
||||
|
||||
kwargs = {}
|
||||
if _HAS_RESOURCE_TRACK:
|
||||
# deprecated: description='track argument for SharedMemory always available' python_version='3.12'
|
||||
kwargs['track'] = False
|
||||
self.shm = shm = SharedMemory(create=True, size=16384, **kwargs) # type: ignore[arg-type]
|
||||
self.shm = shm = SharedMemory(create=True, size=16384, track=False)
|
||||
|
||||
sshpass_prompt = self.get_option('sshpass_prompt')
|
||||
if not sshpass_prompt and pkcs11_provider:
|
||||
|
||||
@@ -174,8 +174,7 @@ class FilterUserInjector(logging.Filter):
|
||||
|
||||
try:
|
||||
username = getpass.getuser()
|
||||
except (ImportError, KeyError, OSError):
|
||||
# deprecated: description='only OSError is required for Python 3.13+' python_version='3.12'
|
||||
except OSError:
|
||||
# people like to make containers w/o actual valid passwd/shadow and use host uids
|
||||
username = 'uid=%s' % os.getuid()
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from __future__ import annotations
|
||||
import random
|
||||
import secrets
|
||||
import string
|
||||
import warnings
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -19,10 +18,6 @@ PASSLIB_E = None
|
||||
PASSLIB_AVAILABLE = False
|
||||
|
||||
try:
|
||||
# deprecated: description='warning suppression only required for Python 3.12 and earlier' python_version='3.12'
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore', message="'crypt' is deprecated and slated for removal in Python 3.13", category=DeprecationWarning)
|
||||
|
||||
import passlib
|
||||
import passlib.hash
|
||||
from passlib.utils.handlers import HasRawSalt, PrefixWrapper
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ requires = ["setuptools >= 77.0.3, <= 80.3.1"] # lower bound to support license
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
requires-python = ">=3.12"
|
||||
requires-python = ">=3.13"
|
||||
name = "ansible-core"
|
||||
authors = [
|
||||
{name = "Ansible Project"},
|
||||
@@ -24,9 +24,9 @@ classifiers = [
|
||||
"Natural Language :: English",
|
||||
"Operating System :: POSIX",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Programming Language :: Python :: 3.15",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Topic :: System :: Installation/Setup",
|
||||
"Topic :: System :: Systems Administration",
|
||||
|
||||
@@ -156,28 +156,13 @@ def get_test_scenarios() -> list[TestScenario]:
|
||||
image = settings['image']
|
||||
cgroup = settings.get('cgroup', 'v1-v2')
|
||||
|
||||
if container_name == 'centos6' and os_release.id == 'alpine':
|
||||
# Alpine kernels do not emulate vsyscall by default, which causes the centos6 container to fail during init.
|
||||
# See: https://unix.stackexchange.com/questions/478387/running-a-centos-docker-image-on-arch-linux-exits-with-code-139
|
||||
# Other distributions enable settings which trap vsyscall by default.
|
||||
# See: https://www.kernelconfig.io/config_legacy_vsyscall_xonly
|
||||
# See: https://www.kernelconfig.io/config_legacy_vsyscall_emulate
|
||||
continue
|
||||
|
||||
for engine in available_engines:
|
||||
# TODO: figure out how to get tests passing using docker without disabling selinux
|
||||
disable_selinux = os_release.id == 'fedora' and engine == 'docker' and cgroup != 'none'
|
||||
debug_systemd = cgroup != 'none'
|
||||
|
||||
# The sleep+pkill used to support the cgroup probe causes problems with the centos6 container.
|
||||
# It results in sshd connections being refused or reset for many, but not all, container instances.
|
||||
# The underlying cause of this issue is unknown.
|
||||
probe_cgroups = container_name != 'centos6'
|
||||
|
||||
# The default RHEL 9 crypto policy prevents use of SHA-1.
|
||||
# This results in SSH errors with centos6 containers: ssh_dispatch_run_fatal: Connection to 1.2.3.4 port 22: error in libcrypto
|
||||
# See: https://access.redhat.com/solutions/6816771
|
||||
enable_sha1 = os_release.id == 'rhel' and os_release.version_id.startswith('9.') and container_name == 'centos6'
|
||||
if engine == 'docker' and container_name.startswith('alpine'):
|
||||
continue # TODO: restore Docker testing of Alpine once it's able to be used as a controller again (probably Alpine 3.24)
|
||||
|
||||
# The AppArmor policy for pasta on Ubuntu 26.04 prevents podman from stopping containers.
|
||||
# Attempting to do so fails with an error like:
|
||||
@@ -237,9 +222,7 @@ def get_test_scenarios() -> list[TestScenario]:
|
||||
image=image,
|
||||
disable_selinux=disable_selinux,
|
||||
expose_cgroup_version=expose_cgroup_version,
|
||||
enable_sha1=enable_sha1,
|
||||
debug_systemd=debug_systemd,
|
||||
probe_cgroups=probe_cgroups,
|
||||
disable_apparmor_profile_pasta=disable_apparmor_profile_pasta,
|
||||
)
|
||||
)
|
||||
@@ -255,16 +238,19 @@ def run_test(scenario: TestScenario) -> TestResult:
|
||||
|
||||
integration = ['ansible-test', 'integration', 'split']
|
||||
integration_options = ['--target', f'docker:{scenario.container_name}', '--color', '--truncate', '0', '-v']
|
||||
target_only_options = []
|
||||
|
||||
if scenario.debug_systemd:
|
||||
integration_options.append('--dev-systemd-debug')
|
||||
|
||||
if scenario.probe_cgroups:
|
||||
target_only_options = ['--dev-probe-cgroups', str(LOG_PATH)]
|
||||
|
||||
entries = get_container_completion_entries()
|
||||
alpine_container = [name for name in entries if name.startswith('alpine')][0]
|
||||
|
||||
# For the split test, Alpine Linux is preferred as the controller. There are two reasons for this:
|
||||
# 1) It doesn't require the cgroup v1 hack, so we can test a target that doesn't need that.
|
||||
# 2) It doesn't require disabling selinux, so we can test a target that doesn't need that.
|
||||
# Unfortunately, this isn't always possible, such as when an Alpine release isn't available with support for controller Python versions.
|
||||
controller_container = [name for name in entries if name.startswith('base')][0]
|
||||
|
||||
commands = [
|
||||
# The cgroup probe is only performed for the first test of the target.
|
||||
@@ -272,10 +258,7 @@ def run_test(scenario: TestScenario) -> TestResult:
|
||||
# The controller will be tested separately as a target.
|
||||
# This ensures that both the probe and no-probe code paths are functional.
|
||||
[*integration, *integration_options, *target_only_options],
|
||||
# For the split test we'll use Alpine Linux as the controller. There are two reasons for this:
|
||||
# 1) It doesn't require the cgroup v1 hack, so we can test a target that doesn't need that.
|
||||
# 2) It doesn't require disabling selinux, so we can test a target that doesn't need that.
|
||||
[*integration, '--controller', f'docker:{alpine_container}', *integration_options],
|
||||
[*integration, '--controller', f'docker:{controller_container}', *integration_options],
|
||||
]
|
||||
|
||||
common_env: dict[str, str] = {}
|
||||
@@ -332,9 +315,6 @@ def run_test(scenario: TestScenario) -> TestResult:
|
||||
if scenario.disable_selinux:
|
||||
run_command('setenforce', 'permissive')
|
||||
|
||||
if scenario.enable_sha1:
|
||||
run_command('update-crypto-policies', '--set', 'DEFAULT:SHA1')
|
||||
|
||||
if scenario.disable_apparmor_profile_pasta:
|
||||
os.symlink('/etc/apparmor.d/usr.bin.pasta', '/etc/apparmor.d/disable/usr.bin.pasta')
|
||||
run_command('apparmor_parser', '-R', '/etc/apparmor.d/usr.bin.pasta')
|
||||
@@ -365,9 +345,6 @@ def run_test(scenario: TestScenario) -> TestResult:
|
||||
os.unlink('/etc/apparmor.d/disable/usr.bin.pasta')
|
||||
run_command('apparmor_parser', '/etc/apparmor.d/usr.bin.pasta')
|
||||
|
||||
if scenario.enable_sha1:
|
||||
run_command('update-crypto-policies', '--set', 'DEFAULT')
|
||||
|
||||
if scenario.disable_selinux:
|
||||
run_command('setenforce', 'enforcing')
|
||||
|
||||
@@ -621,9 +598,7 @@ class TestScenario:
|
||||
image: str
|
||||
disable_selinux: bool
|
||||
expose_cgroup_version: int | None
|
||||
enable_sha1: bool
|
||||
debug_systemd: bool
|
||||
probe_cgroups: bool
|
||||
disable_apparmor_profile_pasta: bool
|
||||
|
||||
@property
|
||||
@@ -642,9 +617,6 @@ class TestScenario:
|
||||
if self.expose_cgroup_version is not None:
|
||||
tags.append(f'cgroup: {self.expose_cgroup_version}')
|
||||
|
||||
if self.enable_sha1:
|
||||
tags.append('sha1: enabled')
|
||||
|
||||
if self.disable_apparmor_profile_pasta:
|
||||
tags.append('apparmor(pasta): disabled')
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ macos/26.3 python=3.14 python_dir=/usr/local/bin become=sudo provider=mac arch=a
|
||||
macos python_dir=/usr/local/bin become=sudo provider=mac arch=aarch64
|
||||
rhel/8.10 python=3.12 become=sudo provider=aws arch=x86_64 alias=rhel/8
|
||||
rhel/9.7 python=3.9,3.12 become=sudo provider=aws arch=x86_64 alias=rhel/9
|
||||
rhel/10.1 python=3.12 become=sudo provider=aws arch=x86_64 alias=rhel/10,rhel/latest
|
||||
rhel/10.1 python=3.12,3.14 become=sudo provider=aws arch=x86_64 alias=rhel/10,rhel/latest
|
||||
rhel become=sudo provider=aws arch=x86_64
|
||||
ubuntu/24.04 python=3.12 become=sudo provider=aws arch=x86_64
|
||||
ubuntu/26.04 python=3.14 become=sudo provider=aws arch=x86_64 alias=ubuntu/latest
|
||||
|
||||
@@ -8,10 +8,10 @@ REMOTE_ONLY_PYTHON_VERSIONS = (
|
||||
'3.9',
|
||||
'3.10',
|
||||
'3.11',
|
||||
'3.12',
|
||||
)
|
||||
|
||||
CONTROLLER_PYTHON_VERSIONS = (
|
||||
'3.12',
|
||||
'3.13',
|
||||
'3.14',
|
||||
'3.15',
|
||||
|
||||
@@ -305,11 +305,6 @@ bootstrap_remote_freebsd()
|
||||
cryptography_pkg="" # not available
|
||||
pyyaml_pkg="" # not available
|
||||
;;
|
||||
"15.0/3.12")
|
||||
jinja2_pkg="" # not available
|
||||
cryptography_pkg="" # not available
|
||||
pyyaml_pkg="" # not available
|
||||
;;
|
||||
*)
|
||||
# just assume nothing is available
|
||||
jinja2_pkg="" # not available
|
||||
@@ -429,7 +424,11 @@ bootstrap_remote_rhel_10()
|
||||
{
|
||||
optimize_dnf
|
||||
|
||||
if [ "${python_version}" = "3.12" ]; then
|
||||
py_pkg_prefix="python3"
|
||||
else
|
||||
py_pkg_prefix="python${python_version}"
|
||||
fi
|
||||
|
||||
packages="
|
||||
gcc
|
||||
@@ -437,14 +436,13 @@ bootstrap_remote_rhel_10()
|
||||
${py_pkg_prefix}-pip
|
||||
"
|
||||
|
||||
# Jinja2, packaging and resolvelib are missing for controller supported Python versions, so we just
|
||||
# skip them and let ansible-test install them from PyPI.
|
||||
if [ "${controller}" ]; then
|
||||
packages="
|
||||
${packages}
|
||||
${py_pkg_prefix}-cryptography
|
||||
${py_pkg_prefix}-jinja2
|
||||
${py_pkg_prefix}-packaging
|
||||
${py_pkg_prefix}-pyyaml
|
||||
${py_pkg_prefix}-resolvelib
|
||||
"
|
||||
fi
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ lib/ansible/plugins/cache/base.py ansible-doc!skip # not a plugin, but a stub f
|
||||
lib/ansible/plugins/callback/__init__.py pylint:arguments-renamed
|
||||
lib/ansible/plugins/inventory/advanced_host_list.py pylint:arguments-renamed
|
||||
lib/ansible/plugins/inventory/host_list.py pylint:arguments-renamed
|
||||
lib/ansible/_internal/_wrapt.py mypy-3.12!skip # vendored code
|
||||
lib/ansible/_internal/_wrapt.py mypy-3.13!skip # vendored code
|
||||
lib/ansible/_internal/_wrapt.py mypy-3.14!skip # vendored code
|
||||
lib/ansible/_internal/_wrapt.py mypy-3.15!skip # vendored code
|
||||
|
||||
@@ -18,13 +18,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
try:
|
||||
# deprecated: description='warning suppression only required for Python 3.12 and earlier' python_version='3.12'
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore', message="'crypt' is deprecated and slated for removal in Python 3.13", category=DeprecationWarning)
|
||||
|
||||
import passlib
|
||||
from passlib.handlers import pbkdf2
|
||||
except ImportError: # pragma: nocover
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
bcrypt < 5 ; python_version >= '3.12' # controller only, bcrypt 5+ not compatible with passlib
|
||||
passlib ; python_version >= '3.12' # controller only
|
||||
pexpect ; python_version >= '3.12' # controller only
|
||||
pywinrm ; python_version >= '3.12' # controller only
|
||||
bcrypt < 5 ; python_version >= '3.13' # controller only, bcrypt 5+ not compatible with passlib
|
||||
passlib ; python_version >= '3.13' # controller only
|
||||
pexpect ; python_version >= '3.13' # controller only
|
||||
pywinrm ; python_version >= '3.13' # controller only
|
||||
typing_extensions; python_version < '3.11' # some unit tests need Annotated and get_type_hints(include_extras=True)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
@@ -228,10 +226,6 @@ def test_random_salt():
|
||||
|
||||
|
||||
def test_passlib_bcrypt_salt(recwarn):
|
||||
# deprecated: description='warning suppression only required for Python 3.12 and earlier' python_version='3.12'
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore', message="'crypt' is deprecated and slated for removal in Python 3.13", category=DeprecationWarning)
|
||||
|
||||
passlib_exc = pytest.importorskip("passlib.exc")
|
||||
|
||||
secret = 'foo'
|
||||
|
||||
Reference in New Issue
Block a user