Krikri::PythonModuleRunner
Constants
The ansible/module_utils bundle a new-style module's
from ansible.module_utils.basic import AnsibleModule import needs.
Real Ansible never relies on ansible-core being installed on the
target - the AnsiballZ wrapper bundles module_utils INTO the module
payload it ships - so every new-style role-private module runs on
any target with a python3. This engine runs the raw module script
instead, so on a target with no ansible-core installed the import
died with ModuleNotFoundError and the module printed no result JSON
("MODULE FAILURE") - hard-FAILING the task where real Ansible ran
it successfully (found via newrelic.newrelic-infra's own
"Setup agent config *NIX" task: the role ships its own
library/merge_yaml.py, which took the py_module path and failed on
every fresh target while real ansible-playbook succeeded). The shim
covers what corpus role-private modules actually use - params
parsing/validation against argument_spec (with type coercion,
defaults, aliases, required), check_mode, exit_json/fail_json,
warn/run_command, log, get_bin_path - plus the
ansible/module_utils/_text helpers modules import directly; not
the whole real basic.py surface; anything beyond that fails
exactly as before this shim existed.
In modern ansible-core the real text-conversion implementation
moved from ansible/module_utils/_text.py to
ansible/module_utils/common/text/converters.py - _text remains
only as a deprecated re-export shim - and newer roles import the
new path directly. bodsch.users' own library/multi_users.py does
exactly that (from ansible.module_utils.common.text.converters import to_native, round 813275) and died with
ModuleNotFoundError: No module named 'ansible.module_utils.common'
on a target without ansible-core, while real Ansible - which ships
both paths - succeeded on the same task. So this file ships
alongside _text.py, self-contained rather than importing from it,
since role code may import either path (or both) and real Ansible
keeps both importable. Same to_bytes/to_text/to_native surface and
the same surrogateescape mapping of the Ansible error-handler
spellings as the _text shim above.
The ansible/module_utils/_text helpers a new-style module can
import DIRECTLY alongside basic (nbde_server_tang via
linux-system-roles.nbde_server does
from ansible.module_utils._text import to_native) - without
this file the import dies with ModuleNotFoundError at module top
level, before AnsibleModule is ever constructed. Only the
to_bytes/to_text/to_native surface modules actually import; the
error-handler spellings real _text.py maps (surrogate_or_strict
et al) become surrogateescape on py3 like the real code.
Class methods
Writes the shim bundle above into work_dir as a real ansible/module_utils package tree. The module script itself sits in work_dir too, and Python puts the script's own directory first on sys.path - so the shim shadows any installed ansible-core exactly when it's written, and the import resolves to it instead of dying with ModuleNotFoundError. Written ONLY for a target where the probe import failed (see py_module.cr): where real ansible-core IS installed the module keeps running against the real basic.py, unchanged from pre-shim behavior.
Instance methods
The old-style key=value argv line (one entry per param).
The module's argument dict: the substituted task params (already
stringified by the parser) re-typed as JSON where they parse -
the parser JSON-encodes list/dict-valued params verbatim, so
"['a','b']" becomes a real array for the module, the way real
Ansible passes typed args. Plus real Ansible's own reserved
_ansible_* keys a new-style module's AnsibleModule reads.
Finds a role-private module source for module_name, or nil.
Search roots mirror real Ansible's two most-used locations: the
current role's own library/ and the playbook-adjacent
library/. First match wins (real Ansible's own nearest-first
order).
Takes the role's ROOT directory directly (task.role_path, always
set - see role_loader.cr's task.role_path = role_dir), not
role_files_dir (only set when the role actually ships a files/
subdirectory - existing_dir returns nil otherwise). A role with
no files/ dir at all (linux-system-roles.storage/.logging/
.timesync, none of them ship one) could never resolve its own
library/*.py modules through the old files/-derived path, so
sr_fingerprint/blivet/timesync_provider fell straight back
to "unavailable modules" - the exact scope cut 0.9.819 was
supposed to have already closed for role-private modules. Found
re-testing linux-system-roles.storage/logging/timesync.
Real Ansible refuses to ship a module payload whose first line is
not a #! interpreter line: ActionBase._execute_module's
if not module_shebang and module_style != 'binary' guard raises
"module (name) is missing interpreter line" as a controller-side
AnsibleError (failed task, nothing executed on the target).
New-style modules are exempt in practice - the AnsiballZ wrapper
embeds its own shebang. This engine runs the raw script with the
target's python3 instead of honoring the file's own line, so
without this guard a shebangless old-style module - which real
ansible-playbook FAILS - silently succeeded (found by the
py_module podman-diff edge cases: only the new-style fixture
survived the real side without a shebang).
Real Ansible's own new-style detection (ansiballz): a module importing ansible.module_utils gets its args as a JSON dict (via the ANSIBLE_MODULE_ARGS env var its basic.py reads when no argv is given); everything else is old-style key=value argv.
Parses the module's stdout into its result JSON: real modules print a JSON object (pretty or single-line), possibly preceded by other output (warnings, prints) that real Ansible also strips. Walks backwards from the end for the first offset where a JSON object parse succeeds.