Krikri::TaskExecutor
TaskExecutor - Executes tasks on hosts Orchestrates task execution, variable substitution, and handler management
Constants
Real 2.19's delimiters deprecation ends with a blank line after the Origin block's caret line, the same trailing-blank shape the #722 constant-template warning's footer carries (byte-compared vs 2.19.11, issue #786 - antmelekhin.mysqld_exporter's 1-byte recap diff).
yum/dnf belong here alongside script: real's dnf ACTION plugin resolves
its backend and, when it cannot, RETURNS a failed dict
(result.update({'failed': True, 'msg': ...}) in dnf.py) instead of
raising - so the executor's attempt loop sees an ordinary failed
result and retries it like any other failure (live-verified vs
2.19.11). Their _ansible_action_level-marked results therefore must
NOT take the raised-exception escape from run_until_retries.
copy.cr runs on the target host (uploaded there like every other
plugin, unlike template: - an action plugin that runs on the
controller and already handles this correctly by reading its own
src: file locally before dispatch). A copy: task's src: names a
file on the controller, so copy.cr's own File.exists?(src)
check - a plain local filesystem check, from the perspective of
wherever it's actually running - can never find it once the play
targets a genuinely remote host: the file was never transferred
there. Found via konstruktoid-hardening's "Add cracklib password
list" (its only copy: task with a real src: file, previously
entirely untested territory) - failed with "Source file not found"
citing the exact real (and really-existing-on-the-controller) path.
Read here, on the controller, and forwarded as content: instead -
copy.cr already has a fully-working content-write path (used by
any copy: {content: ..., dest: ...} task), so this reuses it
rather than needing a separate upload mechanism. Left alone for
remote_src: true (Ansible's own remote-to-remote copy,
where src: already refers to a path on the target, not the
controller - reading it here would be wrong) and for a local
connection (copy.cr already runs on the same filesystem as the
controller in that case, so src: already resolves correctly as-is -
converting it anyway would only change check-mode's message
("Would copy SRC to DEST" -> a generic content message) for no
actual correctness gain).
Above this size, embedding the file as a content param string
would make the JSON config too large to safely round-trip through
#execute_remote_plugin's own base64 encoding of the whole config
(see the comment on that call): a real bug found benchmarking
ansible-community.ansible-vault's own "Install Vault" task, which
copy:s a ~530MB downloaded Vault release binary - Crystal
stdlib's Base64.encode_size computes str_size * 4 as native
Int32 arithmetic before the final .to_i, and a base64'd-then-
JSON-escaped-then-base64'd-again 530MB payload comfortably clears
2^31, crashing the whole engine with an unhandled OverflowError
partway through a run - not a graceful per-task failure.
Collection modules with the same verified shape, by FQCN (after alias resolution). x509_certificate_info: path, the two defaults, then content/valid_at null (#367, pacifica.ansible_certinfra's loop over its CA chain).
Modules whose registered per-item results (loop results[]) carry
Ansible's invocation.module_args and whose args this engine can
reproduce from the argspec table (verified against ansible-core 2.19.11).
Real 2.19's constant-template warning on conditionals is a plain [WARNING], not a deprecation: no disable-hint line, and after the Origin block it carries an inline-expression hint and a trailing blank line (byte-compared vs 2.19.11, issue #722).
Data-driven module argument validation (see ArgspecValidator): the failing result JSON for this task's module args, or nil when validation passes or does not apply. Ansible runs these checks inside the module's own AnsibleModule init - i.e. after the action plugin stage, before any module-side file access - which is exactly where the two callers of this hook sit. Core modules whose Ansible module source declares supports_check_mode=False (and whose plugin mirrors the skip).
Modules real executes WITHOUT its python interpreter machinery: raw/command/shell/script hand their payload straight to the connection's shell, so no interpreter is resolved for them and a missing interpreter cannot fail them (every other module reaching the local dispatch is a python-module run as far as the interpreter question goes).
One definition, shared with the batcher's own raw exclusion (TaskBatcher::raw_task?) - raw also aliases to shell, so a second hand-maintained copy here would drift silently.
loop_control.label - what the per-item line shows instead of the
raw item. Rendered against that item's own context, so it can name
a field of the item.
Shared until: retry core - drives task.retries/task.delay attempts
of execute_task_once (or the caller's attempt_body) until the rendered
condition passes. Used both by execute_task_with_retries (single task,
displays its final result) and, per item, by execute_looped_task's
non-batched path (Ansible retries each loop item independently).
The attempt_body Proc overrides the per-attempt body: execute_handler_
plugin_once passes its own single-attempt body so a handler's
until:/retries:/delay: retries too - real prints the same
FAILED - RETRYING callback lines and attempts key for handlers
(issue #620, live-verified vs real ansible-playbook).
ansible-core 2.21 labels an until:/retries: condition failure with the
clause that raised it, exactly like when: ("Task failed: An 'until'
expression failed: <detail>", live-captured vs 2.21.6).
Render task's name: for the "TASK [...]" banner, lazily - only
builds a vars_context (the same expensive facts-merge #execute_task
itself pays for separately) when the name actually needs it, so a
literal (the overwhelming majority) task name costs nothing extra.
Every banner print site used to print task.name raw, which only
ever reflected whatever narrow, early, per-var-source pass had
already substituted into it (include_tasks:'s include_vars:,
include_role:'s vars:) - anything sourced from a role's own
vars/main.yml (round 26/27's __common_binary_basename, itself a
templated vars-file entry) or from ansible_facts/registered vars
stayed literally {{ ... }} in the banner even though the task's
own body rendered correctly, since the body gets a full vars_context
at actual execution time and the banner never did. Rendering here,
right before print, gives the banner the same full context the body
itself is about to use - it's cosmetic-only (a mistake here can't
affect what actually runs), so best-effort: on any substitution
error, fall back to the raw unrendered name rather than raising.
ansible-core 2.21 labels EVERY conditional-expression failure with the
clause that raised it ("Task failed: A 'when' expression failed: <detail>",
live-captured vs 2.21.6; the same clause label rides the [ERROR] chain's
"caused by" stanza). 2.19 printed the detail alone.
Constructors
Class methods
Prints a result's stashed finalization error block, deduped across the whole run exactly like the inline emission path (Ansible's Display dedups by full formatted message - the Origin's file:line makes the text task-unique).
Real ansible runs raw over the connection with a pty (ssh -tt, because raw sends no stdin - issue #490's eendroroy.pyenv capture): the target's stdout and stderr arrive on the SAME pty, merged in execution order with ONLCR having turned every \n into \r\n, and the ssh client's own stderr holds only its mux close notice. Two capture shapes reach this:
- The plugin captured the command on a REAL target-side pty (PtyCommandRunner - needed because a pty changes how isatty-sensitive programs like apt behave, issue #775). Its _krikri_real_pty marker says the stdout already IS that pty stream: nothing to merge or re-translate, and the marker comes back off the result here. The ssh client's close notice is synthesized the same way as for the emulated capture.
- The pipe emulation (
{ ...; } 2>&1, ScriptPtyCapture's pty_merged_command - the fallback for pty-less targets): the plugin's stdout carries both streams in order but still plain \n, and ONLCR gets re-applied here. (The stream merge happens at the source so the plugin's stderr is empty; appending the (empty) stderr stays harmless and keeps results that reached this path without the marker shaped.)
Local raw has no pty, so it keeps the captured streams untouched. Rewrites the hash in place to keep the shaped result's key order intact.
The shell plugin pre-checks the task's executable: locally and
reports the raw errno as rc (2 ENOENT, 13 EACCES) with msg "Error
executing command." - but real's raw action never spawns the
executable itself. It passes it to _low_level_execute_command, which
sends <executable> -c '<cmd>' through the CONNECTION's own login
shell; the missing/inexecutable binary is then reported BY that shell
as an ordinary non-zero exit - 127 for ENOENT, 126 for EACCES - which
the raw action wraps in the plain failed dict: changed: true, msg
"non-zero return code", and none of the spawn report's cmd/exception
keys. The wording differs by branch because the shells differ: local
runs through /bin/sh (dash: "not found"), remote through the
pty-attached ssh login shell - issue #631's capture shows bash's "No
such file or directory" there (dash login shells would word it
differently; not verified). Returns nil when the hash is not a raw
spawn failure, so every other raw path keeps its shape.
The reboot wait loop's UNREACHABLE-vs-FAILED decision, kept pure and public so the exit-137 / empty-stderr case (a probe killed by the exec timeout) stays testable. Returns the ssh error line for the UNREACHABLE result, or nil to keep the FAILED timeout path: Ansible reports UNREACHABLE when its ssh ConnectTimeout expires (a transport failure), but FAILED when the host is still reachable and only the test command keeps failing.
Batch-build (execute_batch_group / execute_looped_task_batched) prepares every member UP FRONT, before the earlier members' results display - printing the [ERROR] chain block during that preparation put it ahead of the PREVIOUS task's changed: line (issue #14, pylabs.nginx: the block surfaced under the previous task's header, ahead of its own result line). Instead the rendered text rides the failed result on this private marker - the same pattern as _ansible_loop_error_emitted - and the display path (finish_single_task / finish_looped_task) prints it just before the fatal/failed line, under the failing task's own banner.
Instance methods
An add_host: action mutated the shared Inventory directly (the
action plugin holds no executor reference), so - unlike every other
mutation site below - it cannot bump @hv_generation itself. Called
from the three ActionPluginManager.execute_action dispatch sites
after a successful action: without this the hostvars/groups caches
stay stale for the REST of the play and hostvars['<new host>']
reads "undefined" where Ansible exposes the host immediately
(verified live vs 2.19.11: a same-play hostvars['dyn1'] read
after add_host resolves with the full magic-var entry).
Hosts a real failure halted in this play whose error state was since cleared via meta: clear_host_errors. Ansible's own documented semantics: "makes them available for targeting in subsequent plays, but not continue execution in the current play" - so, unlike ended_hosts, these stay in halted_hosts (the current play still stops for them) but are excluded from krikri-playbook.cr's cross-play carry-forward the same way.
Subset of halted_hosts that got there via a CLEAN meta: end_host/ end_play, not a real task failure. Ansible's own semantics: "causes the play to end WITHOUT FAILING the host(s)" - such a host must still be excluded from the REST OF THIS PLAY (the existing halted_hosts mechanism already does that for free, including correctly propagating out of block:/rescue:/always: nesting and suppressing its own pending notified handlers - both verified against ansible-playbook to behave identically to a real failure for THIS play), but must NOT be treated as a failure by krikri-playbook.cr's cross-play carry-forward (permanently_failed_ hosts) or count toward the run's overall failed/exit-code status.
Hosts that hit a failed task without ignore_errors: further tasks in
the play are skipped for them (Ansible's default "a failure aborts
the rest of the play for that host" behavior). Public - crystal-
play.cr reads this after #run to carry a failed host forward and
exclude it from every remaining play in the whole run too, not
just the rest of this one (Ansible's actual behavior; see
git log's 0.9.61-found, 0.9.64-fixed cross-cutting engine
gap commit).
True once this play should stop for EVERY host. Also records the decision so krikri-playbook.cr can skip the remaining serial: batches
- Ansible does not start the next batch after an abort
(verified:
serial: 1+ any_errors_fatal with h2 failing runs h1's batch fully, then stops; h3 never runs at all).
ansible-playbook's -v/-vv/-vvv/... count, exposed to task templating as the ansible_verbosity magic var. See #build_vars_context's own assignment for the full rationale.