Skip to content

Instantly share code, notes, and snippets.

@weishi-imbue
Created April 18, 2026 00:26
Show Gist options
  • Select an option

  • Save weishi-imbue/9becb28be1f95c36a9d68a8f93f5c731 to your computer and use it in GitHub Desktop.

Select an option

Save weishi-imbue/9becb28be1f95c36a9d68a8f93f5c731 to your computer and use it in GitHub Desktop.
an-4c5ce5a1: How review detail level affects fixer behavior (impl → CCR → summary fixer vs full-details fixer)
diff --git a/lib/ansible/executor/module_common.py b/lib/ansible/executor/module_common.py
index 43ed850e02f628..f60a1c603c14f2 100644
--- a/lib/ansible/executor/module_common.py
+++ b/lib/ansible/executor/module_common.py
@@ -194,7 +194,7 @@ def invoke_module(modlib_path, temp_path, json_params):
basic._ANSIBLE_ARGS = json_params
%(coverage)s
# Run the module! By importing it as '__main__', it thinks it is executing as a script
- runpy.run_module(mod_name='%(module_fqn)s', init_globals=None, run_name='__main__', alter_sys=True)
+ runpy.run_module(mod_name='%(module_fqn)s', init_globals=dict(_module_fqn='%(module_fqn)s', _modlib_path=modlib_path), run_name='__main__', alter_sys=True)
# Ansible modules must exit themselves
print('{"msg": "New-style module did not handle its own exit", "failed": true}')
@@ -284,7 +284,7 @@ def debug(command, zipped_mod, json_params):
basic._ANSIBLE_ARGS = json_params
# Run the module! By importing it as '__main__', it thinks it is executing as a script
- runpy.run_module(mod_name='%(module_fqn)s', init_globals=None, run_name='__main__', alter_sys=True)
+ runpy.run_module(mod_name='%(module_fqn)s', init_globals=dict(_module_fqn='%(module_fqn)s', _modlib_path=basedir), run_name='__main__', alter_sys=True)
# Ansible modules must exit themselves
print('{"msg": "New-style module did not handle its own exit", "failed": true}')
diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py
index f0d62acc7e1889..4e488e92a53f16 100644
--- a/lib/ansible/module_utils/basic.py
+++ b/lib/ansible/module_utils/basic.py
@@ -74,7 +74,7 @@
HAVE_SELINUX = False
try:
- import selinux
+ from ansible.module_utils.compat import selinux
HAVE_SELINUX = True
except ImportError:
pass
@@ -876,32 +876,29 @@ def load_file_common_arguments(self, params, path=None):
# by selinux.lgetfilecon().
def selinux_mls_enabled(self):
- if not HAVE_SELINUX:
- return False
- if selinux.is_selinux_mls_enabled() == 1:
- return True
- else:
- return False
+ if not hasattr(self, '_selinux_mls_enabled'):
+ if not HAVE_SELINUX:
+ self._selinux_mls_enabled = False
+ else:
+ self._selinux_mls_enabled = selinux.is_selinux_mls_enabled() == 1
+ return self._selinux_mls_enabled
def selinux_enabled(self):
- if not HAVE_SELINUX:
- seenabled = self.get_bin_path('selinuxenabled')
- if seenabled is not None:
- (rc, out, err) = self.run_command(seenabled)
- if rc == 0:
- self.fail_json(msg="Aborting, target uses selinux but python bindings (libselinux-python) aren't installed!")
- return False
- if selinux.is_selinux_enabled() == 1:
- return True
- else:
- return False
+ if not hasattr(self, '_selinux_enabled'):
+ if not HAVE_SELINUX:
+ self._selinux_enabled = False
+ else:
+ self._selinux_enabled = selinux.is_selinux_enabled() == 1
+ return self._selinux_enabled
# Determine whether we need a placeholder for selevel/mls
def selinux_initial_context(self):
- context = [None, None, None]
- if self.selinux_mls_enabled():
- context.append(None)
- return context
+ if not hasattr(self, '_selinux_initial_context'):
+ context = [None, None, None]
+ if self.selinux_mls_enabled():
+ context.append(None)
+ self._selinux_initial_context = context
+ return list(self._selinux_initial_context)
# If selinux fails to find a default, return an array of None
def selinux_default_context(self, path, mode=0):
diff --git a/lib/ansible/module_utils/common/respawn.py b/lib/ansible/module_utils/common/respawn.py
new file mode 100644
index 00000000000000..547f25666620a8
--- /dev/null
+++ b/lib/ansible/module_utils/common/respawn.py
@@ -0,0 +1,67 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import subprocess
+import sys
+
+RESPAWN_SENTINEL = 'ANSIBLE_MODULE_RESPAWNED'
+
+
+def has_respawned():
+ """Return True if the current module process was respawned via respawn_module."""
+ return os.environ.get(RESPAWN_SENTINEL) == '1'
+
+
+def respawn_module(interpreter_path):
+ """
+ Re-execute the current Ansible module under the specified Python interpreter.
+
+ This function will not return - it replaces the current process after
+ the subprocess completes.
+
+ :arg interpreter_path: Path to the Python interpreter to use for re-execution.
+ :raises Exception: If the module has already been respawned.
+ """
+ if has_respawned():
+ raise Exception('module has already been respawned')
+
+ # The payload and arguments are passed via the module's __main__ globals
+ # which are set by the AnsiballZ wrapper
+ payload = _get_module_payload()
+
+ env = os.environ.copy()
+ env[RESPAWN_SENTINEL] = '1'
+
+ rc = subprocess.call([interpreter_path, payload], env=env)
+ sys.exit(rc)
+
+
+def probe_interpreters_for_module(interpreter_paths, module_name):
+ """
+ Find the first interpreter from interpreter_paths that can import module_name.
+
+ :arg interpreter_paths: List of Python interpreter paths to try.
+ :arg module_name: The Python module name to attempt to import.
+ :returns: The path to the first working interpreter, or None.
+ """
+ for interpreter in interpreter_paths:
+ if not os.path.isfile(interpreter):
+ continue
+ try:
+ rc = subprocess.call(
+ [interpreter, '-c', 'import {0}'.format(module_name)],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ if rc == 0:
+ return interpreter
+ except (OSError, IOError):
+ continue
+ return None
+
+
+def _get_module_payload():
+ """Get the path to the current module payload (the AnsiballZ zipfile)."""
+ # When running under AnsiballZ, sys.argv[0] is the path to the payload script
+ return sys.argv[0]
diff --git a/lib/ansible/module_utils/compat/selinux.py b/lib/ansible/module_utils/compat/selinux.py
new file mode 100644
index 00000000000000..37c6444e2ec621
--- /dev/null
+++ b/lib/ansible/module_utils/compat/selinux.py
@@ -0,0 +1,80 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import ctypes
+import ctypes.util
+
+try:
+ _lib_path = ctypes.util.find_library('selinux')
+ if _lib_path is None:
+ raise OSError('unable to load libselinux.so')
+ _selinux = ctypes.cdll.LoadLibrary(_lib_path)
+except OSError:
+ raise ImportError('unable to load libselinux.so')
+
+
+def is_selinux_enabled():
+ return _selinux.is_selinux_enabled()
+
+
+def is_selinux_mls_enabled():
+ return _selinux.is_selinux_mls_enabled()
+
+
+def selinux_getenforcemode():
+ enforcemode = ctypes.c_int()
+ rc = _selinux.selinux_getenforcemode(ctypes.byref(enforcemode))
+ return [rc, enforcemode.value]
+
+
+def lgetfilecon_raw(path):
+ if isinstance(path, bytes):
+ path = path.decode('utf-8', 'surrogateescape')
+ b_path = path.encode('utf-8', 'surrogateescape')
+ context = ctypes.c_char_p()
+ rc = _selinux.lgetfilecon_raw(b_path, ctypes.byref(context))
+ if rc >= 0 and context.value is not None:
+ result = [rc, context.value.decode('utf-8', 'surrogateescape')]
+ _selinux.freecon(context)
+ return result
+ return [rc, '']
+
+
+def matchpathcon(path, mode):
+ if isinstance(path, bytes):
+ path = path.decode('utf-8', 'surrogateescape')
+ b_path = path.encode('utf-8', 'surrogateescape')
+ context = ctypes.c_char_p()
+ rc = _selinux.matchpathcon(b_path, ctypes.c_int(mode), ctypes.byref(context))
+ if rc >= 0 and context.value is not None:
+ result = [rc, context.value.decode('utf-8', 'surrogateescape')]
+ _selinux.freecon(context)
+ return result
+ return [rc, '']
+
+
+def lsetfilecon(path, context):
+ if isinstance(path, bytes):
+ path = path.decode('utf-8', 'surrogateescape')
+ if isinstance(context, bytes):
+ context = context.decode('utf-8', 'surrogateescape')
+ b_path = path.encode('utf-8', 'surrogateescape')
+ b_context = context.encode('utf-8', 'surrogateescape')
+ return _selinux.lsetfilecon(b_path, b_context)
+
+
+def security_policyvers():
+ return _selinux.security_policyvers()
+
+
+def security_getenforce():
+ return _selinux.security_getenforce()
+
+
+def selinux_getpolicytype():
+ policytype = ctypes.c_char_p()
+ rc = _selinux.selinux_getpolicytype(ctypes.byref(policytype))
+ if rc == 0 and policytype.value is not None:
+ result = [rc, policytype.value.decode('utf-8', 'surrogateescape')]
+ return result
+ return [rc, '']
diff --git a/lib/ansible/module_utils/facts/system/selinux.py b/lib/ansible/module_utils/facts/system/selinux.py
index c3f88fa979d682..d3aa89ded2ef4d 100644
--- a/lib/ansible/module_utils/facts/system/selinux.py
+++ b/lib/ansible/module_utils/facts/system/selinux.py
@@ -21,7 +21,7 @@
from ansible.module_utils.facts.collector import BaseFactCollector
try:
- import selinux
+ from ansible.module_utils.compat import selinux
HAVE_SELINUX = True
except ImportError:
HAVE_SELINUX = False
diff --git a/lib/ansible/modules/apt.py b/lib/ansible/modules/apt.py
index cd36b622310bd9..04b15a2ce89770 100644
--- a/lib/ansible/modules/apt.py
+++ b/lib/ansible/modules/apt.py
@@ -323,6 +323,7 @@
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.urls import fetch_file
+from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
# APT related constants
APT_ENV_VARS = dict(
@@ -1088,6 +1089,11 @@ def main():
module.run_command_environ_update = APT_ENV_VARS
if not HAS_PYTHON_APT:
+ if not has_respawned():
+ interpreter = probe_interpreters_for_module(
+ ['/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'apt')
+ if interpreter:
+ respawn_module(interpreter)
if module.check_mode:
module.fail_json(msg="%s must be installed to use check mode. "
"If run normally this module can auto-install it." % PYTHON_APT)
@@ -1106,8 +1112,7 @@ def main():
import apt.debfile
import apt_pkg
except ImportError:
- module.fail_json(msg="Could not import python modules: apt, apt_pkg. "
- "Please install %s package." % PYTHON_APT)
+ module.fail_json(msg="{0} must be installed and visible from {1}.".format(PYTHON_APT, sys.executable))
global APTITUDE_CMD
APTITUDE_CMD = module.get_bin_path("aptitude", False)
diff --git a/lib/ansible/modules/apt_repository.py b/lib/ansible/modules/apt_repository.py
index 36fe8cf5ce9789..692a78ae0145e8 100644
--- a/lib/ansible/modules/apt_repository.py
+++ b/lib/ansible/modules/apt_repository.py
@@ -153,6 +153,7 @@
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.urls import fetch_url
+from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
if sys.version_info[0] < 3:
@@ -184,7 +185,8 @@ def install_python_apt(module):
else:
module.fail_json(msg="Failed to auto-install %s. Error was: '%s'" % (PYTHON_APT, se.strip()))
else:
- module.fail_json(msg="%s must be installed to use check mode" % PYTHON_APT)
+ module.fail_json(msg="%s must be installed to use check mode. "
+ "If run normally this module can auto-install it." % PYTHON_APT)
class InvalidSource(Exception):
@@ -552,10 +554,16 @@ def main():
sourceslist = None
if not HAVE_PYTHON_APT:
+ if not has_respawned():
+ interpreter = probe_interpreters_for_module(
+ ['/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'apt')
+ if interpreter:
+ respawn_module(interpreter)
if params['install_python_apt']:
install_python_apt(module)
else:
- module.fail_json(msg='%s is not installed, and install_python_apt is False' % PYTHON_APT)
+ if not HAVE_PYTHON_APT:
+ module.fail_json(msg="{0} must be installed and visible from {1}.".format(PYTHON_APT, sys.executable))
if not repo:
module.fail_json(msg='Please set argument \'repo\' to a non-empty value')
diff --git a/lib/ansible/modules/dnf.py b/lib/ansible/modules/dnf.py
index 7550b3371fb0ac..80474fa116332c 100644
--- a/lib/ansible/modules/dnf.py
+++ b/lib/ansible/modules/dnf.py
@@ -342,6 +342,7 @@
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec
+from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
class DnfModule(YumDnf):
@@ -515,6 +516,12 @@ def _ensure_dnf(self):
else:
package = 'python3-dnf'
+ candidate_interpreters = ['/usr/libexec/platform-python', '/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python']
+ if not has_respawned():
+ interpreter = probe_interpreters_for_module(candidate_interpreters, 'dnf')
+ if interpreter:
+ respawn_module(interpreter)
+
if self.module.check_mode:
self.module.fail_json(
msg="`{0}` is not installed, but it is required"
@@ -534,9 +541,11 @@ def _ensure_dnf(self):
except ImportError:
self.module.fail_json(
msg="Could not import the dnf python module using {0} ({1}). "
- "Please install `{2}` package or ensure you have specified the "
- "correct ansible_python_interpreter.".format(sys.executable, sys.version.replace('\n', ''),
- package),
+ "Please install `python3-dnf` or `python2-dnf` package or ensure you have specified the "
+ "correct ansible_python_interpreter. (attempted {2})".format(
+ sys.executable,
+ sys.version.replace('\n', ''),
+ candidate_interpreters),
results=[],
cmd='dnf install -y {0}'.format(package),
rc=rc,
diff --git a/lib/ansible/modules/package_facts.py b/lib/ansible/modules/package_facts.py
index 0575f5c4eda243..be4b3f89c2c1fc 100644
--- a/lib/ansible/modules/package_facts.py
+++ b/lib/ansible/modules/package_facts.py
@@ -209,9 +209,12 @@
import re
+import sys
+
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.process import get_bin_path
+from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
from ansible.module_utils.facts.packages import LibMgr, CLIMgr, get_all_pkg_managers
@@ -233,10 +236,16 @@ def is_available(self):
''' we expect the python bindings installed, but this gives warning if they are missing and we have rpm cli'''
we_have_lib = super(RPM, self).is_available()
+ if not we_have_lib and not has_respawned():
+ interpreter = probe_interpreters_for_module(
+ ['/usr/libexec/platform-python', '/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'rpm')
+ if interpreter:
+ respawn_module(interpreter)
+
try:
get_bin_path('rpm')
if not we_have_lib:
- module.warn('Found "rpm" but %s' % (missing_required_lib('rpm')))
+ module.warn('Found "rpm" but %s' % (missing_required_lib(self.LIB)))
except ValueError:
pass
@@ -263,6 +272,11 @@ def is_available(self):
''' we expect the python bindings installed, but if there is apt/apt-get give warning about missing bindings'''
we_have_lib = super(APT, self).is_available()
if not we_have_lib:
+ if not has_respawned():
+ interpreter = probe_interpreters_for_module(
+ ['/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'apt')
+ if interpreter:
+ respawn_module(interpreter)
for exe in ('apt', 'apt-get', 'aptitude'):
try:
get_bin_path(exe)
diff --git a/lib/ansible/modules/yum.py b/lib/ansible/modules/yum.py
index 3a74eced0fb019..d711a2672b9100 100644
--- a/lib/ansible/modules/yum.py
+++ b/lib/ansible/modules/yum.py
@@ -373,6 +373,7 @@
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec
+from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
import errno
import os
@@ -1599,10 +1600,19 @@ def run(self):
"""
error_msgs = []
+ if not HAS_RPM_PYTHON or not HAS_YUM_PYTHON:
+ if sys.executable != '/usr/bin/python' and not has_respawned():
+ interpreter = probe_interpreters_for_module(
+ ['/usr/libexec/platform-python', '/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'yum')
+ if interpreter:
+ respawn_module(interpreter)
+
if not HAS_RPM_PYTHON:
- error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.')
+ error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.. '
+ 'Please install the python-rpm package or ensure you have specified the correct ansible_python_interpreter. ({0})'.format(sys.executable))
if not HAS_YUM_PYTHON:
- error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.')
+ error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.. '
+ 'Please install the yum package or ensure you have specified the correct ansible_python_interpreter. ({0})'.format(sys.executable))
self.wait_for_lock()

Claude Code Review (full, with extended reasoning)

Review Comment 1: lib/ansible/module_utils/compat/selinux.py:80

🔴 In selinux_getpolicytype() (lib/ansible/module_utils/compat/selinux.py:74-80), the C string allocated by _selinux.selinux_getpolicytype() is never freed with freecon(), unlike lgetfilecon_raw() and matchpathcon() in the same file which both correctly call _selinux.freecon(context) before returning. The fix is to add _selinux.freecon(policytype) after decoding the value and before return result.

Extended reasoning...

What the bug is

The selinux_getpolicytype() function in the new compat layer calls _selinux.selinux_getpolicytype(ctypes.byref(policytype)). The C function allocates a string internally (via strdup()) and stores the pointer in the c_char_p buffer passed by reference. The caller is responsible for freeing this memory using freecon().

The specific code path

Lines 74-80 of lib/ansible/module_utils/compat/selinux.py:

def selinux_getpolicytype():
    policytype = ctypes.c_char_p()
    rc = _selinux.selinux_getpolicytype(ctypes.byref(policytype))
    if rc == 0 and policytype.value is not None:
        result = [rc, policytype.value.decode('utf-8', 'surrogateescape')]
        return result  # freecon(policytype) missing here
    return [rc, '']

Why existing code does not prevent it

lgetfilecon_raw() (line 38) and matchpathcon() (line 51) both correctly call _selinux.freecon(context) after decoding the value and before returning. Python's garbage collector frees the ctypes.c_char_p Python wrapper object but does NOT free the underlying C-heap memory, because ctypes does not own allocations made by C library functions.

Impact

Every call to selinux_getpolicytype() leaks the C-allocated policy type string. This is called from SelinuxFactCollector.collect() in facts/system/selinux.py on every fact collection run on SELinux-enabled systems. While Ansible modules are short-lived and the OS reclaims memory at exit, the leak is real and violates the libselinux API contract.

How to fix it

Add _selinux.freecon(policytype) after decoding the value:

def selinux_getpolicytype():
    policytype = ctypes.c_char_p()
    rc = _selinux.selinux_getpolicytype(ctypes.byref(policytype))
    if rc == 0 and policytype.value is not None:
        result = [rc, policytype.value.decode('utf-8', 'surrogateescape')]
        _selinux.freecon(policytype)  # add this line
        return result
    return [rc, '']

Step-by-step proof

  1. Ansible collects facts on a SELinux-enabled system, calling SelinuxFactCollector.collect().
  2. collect() calls selinux.selinux_getpolicytype() (facts/system/selinux.py line 79).
  3. The C function allocates e.g. "targeted\0" via strdup() and stores the pointer in policytype.
  4. policytype.value.decode(...) produces a Python string; result holds [0, "targeted"].
  5. return result executes — the C allocation is never passed to freecon() and leaks.
  6. On every subsequent fact collection run, another allocation is made and leaked.

Review Comment 2: general

🟡 Both new error messages in yum.py contain a double period ('instead.. Please') because the original sentence ending in '.' was concatenated with a new sentence starting with '. Please', producing malformed user-facing output. Fix by removing the trailing period before the continuation text in both strings at lines 1611 and 1614.

Extended reasoning...

The bug is a straightforward typo introduced in this PR in lib/ansible/modules/yum.py. The original error strings ended cleanly with a single period: '...use the dnf Ansible module instead.' The PR replaced them with longer strings that append additional guidance, but failed to remove the trailing period before appending the new sentence.

The resulting strings contain 'instead.. Please' -- two consecutive periods where one is expected. This is visible in both the HAS_RPM_PYTHON and HAS_YUM_PYTHON error messages at lines 1611 and 1614.

The specific code path: when the yum module runs on a system where either HAS_RPM_PYTHON or HAS_YUM_PYTHON is False (i.e., the required Python 2 bindings are not present), these error messages are appended to error_msgs and eventually surfaced to the user via self.module.fail_json(msg='. '.join(error_msgs)). Any user missing the RPM or yum Python bindings will see the double period in the failure message.

Existing code does not prevent this because it is purely a string literal typo -- no validation of error message strings takes place.

The impact is cosmetic: users see 'instead.. Please' instead of 'instead. Please', which looks unprofessional and may cause confusion, but the message is still intelligible and functionality is unaffected.

Step-by-step proof:

  1. On a Python 3 system, 'import rpm' fails so HAS_RPM_PYTHON = False.
  2. run() is invoked; respawn logic finds no suitable interpreter and falls through.
  3. error_msgs.append('...instead.. Please install the python-rpm package...') is executed (line 1611).
  4. self.module.fail_json(msg='. '.join(error_msgs)) is called with the double-period string.
  5. The user sees: 'The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the dnf Ansible module instead.. Please install the python-rpm package...'

Fix: change 'instead.. ' to 'instead. ' in both string literals at lines 1611 and 1614.

Review Comment 3: general

🟡 In apt_repository.py main(), the else branch of if params['install_python_apt']: wraps module.fail_json(...) in a redundant inner if not HAVE_PYTHON_APT: guard that is always True — we are already inside the outer if not HAVE_PYTHON_APT: block and nothing in the else-branch modifies that global. The original code called module.fail_json directly; the new inner condition is dead code that obscures intent and may suggest the developer meant to check after install_python_apt() ran (which only executes in the other branch).

Extended reasoning...

What the bug is and how it manifests

In lib/ansible/modules/apt_repository.py, the PR restructured the missing-apt handling inside main() to add respawn logic. The resulting code structure is roughly:

if not HAVE_PYTHON_APT: # outer — HAVE_PYTHON_APT is False here if not has_respawned(): interpreter = probe_interpreters_for_module(...) if interpreter: respawn_module(interpreter) if params['install_python_apt']: install_python_apt(module) else: if not HAVE_PYTHON_APT: # inner — always True, redundant module.fail_json(...)

The inner if not HAVE_PYTHON_APT: at the else-branch is always True, making it dead code.

The specific code path that triggers it

This branch is reached when: (1) apt cannot be imported at module load time, (2) either the module has already been respawned or no suitable interpreter was found, and (3) the user set install_python_apt=False. In that scenario, execution falls into the else: block and hits the always-True guard.

Why existing code does not prevent it

HAVE_PYTHON_APT is a module-level global set once at import time. Inside the outer if not HAVE_PYTHON_APT: block, the only statement that sets it to True is install_python_apt(module) — and that function is only called in the if params['install_python_apt']: branch, not the else: branch under examination. respawn_module() either calls sys.exit() (replacing the process) or raises an exception; it never returns and cannot affect the global. Therefore HAVE_PYTHON_APT is still False when the inner check is evaluated.

Impact

No functional impact — module.fail_json(...) is still always reached in this branch. However, the spurious guard is misleading dead code that implies HAVE_PYTHON_APT could theoretically be True at that point. It also diverges from the analogous pattern in apt.py (same PR), which calls module.fail_json() directly without a second check.

How to fix

Remove the inner if not HAVE_PYTHON_APT: guard and call module.fail_json(...) directly:

else: module.fail_json(msg="{0} must be installed and visible from {1}.".format(PYTHON_APT, sys.executable))

Step-by-step proof

  1. Module starts; import apt fails at top of file → HAVE_PYTHON_APT = False.
  2. Execution enters main(). if not HAVE_PYTHON_APT: is True, we enter the outer block.
  3. Either has_respawned() returns True, or probe_interpreters_for_module returns None — either way respawn_module() is not called and we continue.
  4. params['install_python_apt'] is False (user configured this) → we take the else branch.
  5. HAVE_PYTHON_APT is still False; nothing between step 2 and here has modified it.
  6. if not HAVE_PYTHON_APT: evaluates to not False = True — the condition is trivially and always satisfied.
  7. module.fail_json(...) is called. The inner guard filtered nothing and served no purpose.
diff --git a/lib/ansible/module_utils/compat/selinux.py b/lib/ansible/module_utils/compat/selinux.py
index 37c6444e2ec621..458953e7d2af22 100644
--- a/lib/ansible/module_utils/compat/selinux.py
+++ b/lib/ansible/module_utils/compat/selinux.py
@@ -76,5 +76,6 @@ def selinux_getpolicytype():
rc = _selinux.selinux_getpolicytype(ctypes.byref(policytype))
if rc == 0 and policytype.value is not None:
result = [rc, policytype.value.decode('utf-8', 'surrogateescape')]
+ _selinux.freecon(policytype)
return result
return [rc, '']
diff --git a/lib/ansible/modules/apt_repository.py b/lib/ansible/modules/apt_repository.py
index 692a78ae0145e8..53cb140487bea5 100644
--- a/lib/ansible/modules/apt_repository.py
+++ b/lib/ansible/modules/apt_repository.py
@@ -562,8 +562,7 @@ def main():
if params['install_python_apt']:
install_python_apt(module)
else:
- if not HAVE_PYTHON_APT:
- module.fail_json(msg="{0} must be installed and visible from {1}.".format(PYTHON_APT, sys.executable))
+ module.fail_json(msg="{0} must be installed and visible from {1}.".format(PYTHON_APT, sys.executable))
if not repo:
module.fail_json(msg='Please set argument \'repo\' to a non-empty value')
diff --git a/lib/ansible/modules/yum.py b/lib/ansible/modules/yum.py
index d711a2672b9100..c4913239ae5acb 100644
--- a/lib/ansible/modules/yum.py
+++ b/lib/ansible/modules/yum.py
@@ -1608,10 +1608,10 @@ def run(self):
respawn_module(interpreter)
if not HAS_RPM_PYTHON:
- error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.. '
+ error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead. '
'Please install the python-rpm package or ensure you have specified the correct ansible_python_interpreter. ({0})'.format(sys.executable))
if not HAS_YUM_PYTHON:
- error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.. '
+ error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead. '
'Please install the yum package or ensure you have specified the correct ansible_python_interpreter. ({0})'.format(sys.executable))
self.wait_for_lock()
diff --git a/lib/ansible/executor/module_common.py b/lib/ansible/executor/module_common.py
index f60a1c603c14f2..43ed850e02f628 100644
--- a/lib/ansible/executor/module_common.py
+++ b/lib/ansible/executor/module_common.py
@@ -194,7 +194,7 @@ def invoke_module(modlib_path, temp_path, json_params):
basic._ANSIBLE_ARGS = json_params
%(coverage)s
# Run the module! By importing it as '__main__', it thinks it is executing as a script
- runpy.run_module(mod_name='%(module_fqn)s', init_globals=dict(_module_fqn='%(module_fqn)s', _modlib_path=modlib_path), run_name='__main__', alter_sys=True)
+ runpy.run_module(mod_name='%(module_fqn)s', init_globals=None, run_name='__main__', alter_sys=True)
# Ansible modules must exit themselves
print('{"msg": "New-style module did not handle its own exit", "failed": true}')
@@ -284,7 +284,7 @@ def debug(command, zipped_mod, json_params):
basic._ANSIBLE_ARGS = json_params
# Run the module! By importing it as '__main__', it thinks it is executing as a script
- runpy.run_module(mod_name='%(module_fqn)s', init_globals=dict(_module_fqn='%(module_fqn)s', _modlib_path=basedir), run_name='__main__', alter_sys=True)
+ runpy.run_module(mod_name='%(module_fqn)s', init_globals=None, run_name='__main__', alter_sys=True)
# Ansible modules must exit themselves
print('{"msg": "New-style module did not handle its own exit", "failed": true}')
diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py
index 4e488e92a53f16..f0d62acc7e1889 100644
--- a/lib/ansible/module_utils/basic.py
+++ b/lib/ansible/module_utils/basic.py
@@ -74,7 +74,7 @@
HAVE_SELINUX = False
try:
- from ansible.module_utils.compat import selinux
+ import selinux
HAVE_SELINUX = True
except ImportError:
pass
@@ -876,29 +876,32 @@ def load_file_common_arguments(self, params, path=None):
# by selinux.lgetfilecon().
def selinux_mls_enabled(self):
- if not hasattr(self, '_selinux_mls_enabled'):
- if not HAVE_SELINUX:
- self._selinux_mls_enabled = False
- else:
- self._selinux_mls_enabled = selinux.is_selinux_mls_enabled() == 1
- return self._selinux_mls_enabled
+ if not HAVE_SELINUX:
+ return False
+ if selinux.is_selinux_mls_enabled() == 1:
+ return True
+ else:
+ return False
def selinux_enabled(self):
- if not hasattr(self, '_selinux_enabled'):
- if not HAVE_SELINUX:
- self._selinux_enabled = False
- else:
- self._selinux_enabled = selinux.is_selinux_enabled() == 1
- return self._selinux_enabled
+ if not HAVE_SELINUX:
+ seenabled = self.get_bin_path('selinuxenabled')
+ if seenabled is not None:
+ (rc, out, err) = self.run_command(seenabled)
+ if rc == 0:
+ self.fail_json(msg="Aborting, target uses selinux but python bindings (libselinux-python) aren't installed!")
+ return False
+ if selinux.is_selinux_enabled() == 1:
+ return True
+ else:
+ return False
# Determine whether we need a placeholder for selevel/mls
def selinux_initial_context(self):
- if not hasattr(self, '_selinux_initial_context'):
- context = [None, None, None]
- if self.selinux_mls_enabled():
- context.append(None)
- self._selinux_initial_context = context
- return list(self._selinux_initial_context)
+ context = [None, None, None]
+ if self.selinux_mls_enabled():
+ context.append(None)
+ return context
# If selinux fails to find a default, return an array of None
def selinux_default_context(self, path, mode=0):
diff --git a/lib/ansible/module_utils/facts/system/selinux.py b/lib/ansible/module_utils/facts/system/selinux.py
index d3aa89ded2ef4d..c3f88fa979d682 100644
--- a/lib/ansible/module_utils/facts/system/selinux.py
+++ b/lib/ansible/module_utils/facts/system/selinux.py
@@ -21,7 +21,7 @@
from ansible.module_utils.facts.collector import BaseFactCollector
try:
- from ansible.module_utils.compat import selinux
+ import selinux
HAVE_SELINUX = True
except ImportError:
HAVE_SELINUX = False
diff --git a/lib/ansible/modules/apt.py b/lib/ansible/modules/apt.py
index 04b15a2ce89770..cd36b622310bd9 100644
--- a/lib/ansible/modules/apt.py
+++ b/lib/ansible/modules/apt.py
@@ -323,7 +323,6 @@
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.urls import fetch_file
-from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
# APT related constants
APT_ENV_VARS = dict(
@@ -1089,11 +1088,6 @@ def main():
module.run_command_environ_update = APT_ENV_VARS
if not HAS_PYTHON_APT:
- if not has_respawned():
- interpreter = probe_interpreters_for_module(
- ['/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'apt')
- if interpreter:
- respawn_module(interpreter)
if module.check_mode:
module.fail_json(msg="%s must be installed to use check mode. "
"If run normally this module can auto-install it." % PYTHON_APT)
@@ -1112,7 +1106,8 @@ def main():
import apt.debfile
import apt_pkg
except ImportError:
- module.fail_json(msg="{0} must be installed and visible from {1}.".format(PYTHON_APT, sys.executable))
+ module.fail_json(msg="Could not import python modules: apt, apt_pkg. "
+ "Please install %s package." % PYTHON_APT)
global APTITUDE_CMD
APTITUDE_CMD = module.get_bin_path("aptitude", False)
diff --git a/lib/ansible/modules/apt_repository.py b/lib/ansible/modules/apt_repository.py
index 692a78ae0145e8..36fe8cf5ce9789 100644
--- a/lib/ansible/modules/apt_repository.py
+++ b/lib/ansible/modules/apt_repository.py
@@ -153,7 +153,6 @@
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.urls import fetch_url
-from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
if sys.version_info[0] < 3:
@@ -185,8 +184,7 @@ def install_python_apt(module):
else:
module.fail_json(msg="Failed to auto-install %s. Error was: '%s'" % (PYTHON_APT, se.strip()))
else:
- module.fail_json(msg="%s must be installed to use check mode. "
- "If run normally this module can auto-install it." % PYTHON_APT)
+ module.fail_json(msg="%s must be installed to use check mode" % PYTHON_APT)
class InvalidSource(Exception):
@@ -554,16 +552,10 @@ def main():
sourceslist = None
if not HAVE_PYTHON_APT:
- if not has_respawned():
- interpreter = probe_interpreters_for_module(
- ['/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'apt')
- if interpreter:
- respawn_module(interpreter)
if params['install_python_apt']:
install_python_apt(module)
else:
- if not HAVE_PYTHON_APT:
- module.fail_json(msg="{0} must be installed and visible from {1}.".format(PYTHON_APT, sys.executable))
+ module.fail_json(msg='%s is not installed, and install_python_apt is False' % PYTHON_APT)
if not repo:
module.fail_json(msg='Please set argument \'repo\' to a non-empty value')
diff --git a/lib/ansible/modules/dnf.py b/lib/ansible/modules/dnf.py
index 80474fa116332c..7550b3371fb0ac 100644
--- a/lib/ansible/modules/dnf.py
+++ b/lib/ansible/modules/dnf.py
@@ -342,7 +342,6 @@
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec
-from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
class DnfModule(YumDnf):
@@ -516,12 +515,6 @@ def _ensure_dnf(self):
else:
package = 'python3-dnf'
- candidate_interpreters = ['/usr/libexec/platform-python', '/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python']
- if not has_respawned():
- interpreter = probe_interpreters_for_module(candidate_interpreters, 'dnf')
- if interpreter:
- respawn_module(interpreter)
-
if self.module.check_mode:
self.module.fail_json(
msg="`{0}` is not installed, but it is required"
@@ -541,11 +534,9 @@ def _ensure_dnf(self):
except ImportError:
self.module.fail_json(
msg="Could not import the dnf python module using {0} ({1}). "
- "Please install `python3-dnf` or `python2-dnf` package or ensure you have specified the "
- "correct ansible_python_interpreter. (attempted {2})".format(
- sys.executable,
- sys.version.replace('\n', ''),
- candidate_interpreters),
+ "Please install `{2}` package or ensure you have specified the "
+ "correct ansible_python_interpreter.".format(sys.executable, sys.version.replace('\n', ''),
+ package),
results=[],
cmd='dnf install -y {0}'.format(package),
rc=rc,
diff --git a/lib/ansible/modules/package_facts.py b/lib/ansible/modules/package_facts.py
index be4b3f89c2c1fc..0575f5c4eda243 100644
--- a/lib/ansible/modules/package_facts.py
+++ b/lib/ansible/modules/package_facts.py
@@ -209,12 +209,9 @@
import re
-import sys
-
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.process import get_bin_path
-from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
from ansible.module_utils.facts.packages import LibMgr, CLIMgr, get_all_pkg_managers
@@ -236,16 +233,10 @@ def is_available(self):
''' we expect the python bindings installed, but this gives warning if they are missing and we have rpm cli'''
we_have_lib = super(RPM, self).is_available()
- if not we_have_lib and not has_respawned():
- interpreter = probe_interpreters_for_module(
- ['/usr/libexec/platform-python', '/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'rpm')
- if interpreter:
- respawn_module(interpreter)
-
try:
get_bin_path('rpm')
if not we_have_lib:
- module.warn('Found "rpm" but %s' % (missing_required_lib(self.LIB)))
+ module.warn('Found "rpm" but %s' % (missing_required_lib('rpm')))
except ValueError:
pass
@@ -272,11 +263,6 @@ def is_available(self):
''' we expect the python bindings installed, but if there is apt/apt-get give warning about missing bindings'''
we_have_lib = super(APT, self).is_available()
if not we_have_lib:
- if not has_respawned():
- interpreter = probe_interpreters_for_module(
- ['/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'apt')
- if interpreter:
- respawn_module(interpreter)
for exe in ('apt', 'apt-get', 'aptitude'):
try:
get_bin_path(exe)
diff --git a/lib/ansible/modules/yum.py b/lib/ansible/modules/yum.py
index d711a2672b9100..3a74eced0fb019 100644
--- a/lib/ansible/modules/yum.py
+++ b/lib/ansible/modules/yum.py
@@ -373,7 +373,6 @@
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.yumdnf import YumDnf, yumdnf_argument_spec
-from ansible.module_utils.common.respawn import has_respawned, respawn_module, probe_interpreters_for_module
import errno
import os
@@ -1600,19 +1599,10 @@ def run(self):
"""
error_msgs = []
- if not HAS_RPM_PYTHON or not HAS_YUM_PYTHON:
- if sys.executable != '/usr/bin/python' and not has_respawned():
- interpreter = probe_interpreters_for_module(
- ['/usr/libexec/platform-python', '/usr/bin/python3', '/usr/bin/python2', '/usr/bin/python'], 'yum')
- if interpreter:
- respawn_module(interpreter)
-
if not HAS_RPM_PYTHON:
- error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.. '
- 'Please install the python-rpm package or ensure you have specified the correct ansible_python_interpreter. ({0})'.format(sys.executable))
+ error_msgs.append('The Python 2 bindings for rpm are needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.')
if not HAS_YUM_PYTHON:
- error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.. '
- 'Please install the yum package or ensure you have specified the correct ansible_python_interpreter. ({0})'.format(sys.executable))
+ error_msgs.append('The Python 2 yum module is needed for this module. If you require Python 3 support use the `dnf` Ansible module instead.')
self.wait_for_lock()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment