Krikri::ResultDisplay
ResultDisplay - Handles displaying task results and diffs
Constants
Serializes "claim the dedupe key, then print" across the fibers of a --forks run: the claim is the Display-level dedupe every emitter shares, and a replayed block claims late.
Class methods
Display an ad-hoc ansible command's result, matching
ansible's own default ("minimal") callback: host | STATUS | rc=N >>
followed by raw stdout for command-shaped modules (rc + stdout
present - command/shell/script/raw), or host | STATUS => {...}
pretty-printed JSON for every other module. Deliberately NOT
ResultDisplay.display_result - that one renders ansible-playbook's
own "ok: [host]" TASK-recap style, a different output convention
ansible's ad-hoc CLI has never used.
Ad-hoc-only output modifiers, set by krikri.cr (the ansible
counterpart binary) from its own CLI flags:
-o/--one-line switches to Ansible's deprecated oneline
callback shape - everything on ONE line: command-shaped results
render as host | STATUS | rc=N | (stdout) ... with newlines
escaped (verified against ansible-core 2.19.4's
plugins/callback/oneline.py), everything else renders the result
JSON compact (indent=0, newlines stripped) instead of pretty.
There is no krikri-playbook equivalent to mirror - the playbook CLI
has no -o.
Display an ad-hoc ansible command's result, matching
ansible's own default ("minimal") callback: host | STATUS | rc=N >>
followed by raw stdout for command-shaped modules (rc + stdout
present - command/shell/script/raw), or host | STATUS => {...}
pretty-printed JSON for every other module. Deliberately NOT
ResultDisplay.display_result - that one renders ansible-playbook's
own "ok: [host]" TASK-recap style, a different output convention
ansible's ad-hoc CLI has never used.
Ad-hoc-only output modifiers, set by krikri.cr (the ansible
counterpart binary) from its own CLI flags:
-o/--one-line switches to Ansible's deprecated oneline
callback shape - everything on ONE line: command-shaped results
render as host | STATUS | rc=N | (stdout) ... with newlines
escaped (verified against ansible-core 2.19.4's
plugins/callback/oneline.py), everything else renders the result
JSON compact (indent=0, newlines stripped) instead of pretty.
There is no krikri-playbook equivalent to mirror - the playbook CLI
has no -o.
The exact JSON string Ansible's ad-hoc stdout callbacks dump
after the host | STATUS => prefix. The result dict the callbacks
see has already been through Ansible's cleaning pipeline by
the time it is dumped: executor/task_result.py's
as_callback_task_result strips failed/skipped for EVERY stdout
callback (a real FAILED! => dump does not contain "failed": true -
live-verified against 2.19.4), and _dump_results strips the private
_ansible_* keys and dumps with sort_keys=True. A debug task
additionally goes through _clean_results - but ONLY in the minimal
(pretty) path: oneline.py never calls it, so an -o debug dump
keeps its other keys (verified live). Status derivation stays with
the caller - stripping here is display-only.
Returns {state word, SGR color code} for an ad-hoc result, mirroring Ansible's minimal/oneline callbacks: unreachable wins over failed, failed over changed. Public so the state-to-color mapping (especially unreachable's distinct bright red, not plain red) can be regression-tested without a live host.
-t/--tree DIR: additionally log each result as pretty JSON in DIR/<hostname>, like Ansible's tree callback plugin.
-t/--tree DIR: additionally log each result as pretty JSON in DIR/<hostname>, like Ansible's tree callback plugin.
Position (1-based line, 1-based column) of assert's that: item
number index (0-based) within the source, scanned from start_idx.
Public so the templating-delimiters deprecation emitter can point
assert's per-item Origin without going through the ErrorBlock
text form.
ansible-core captures every warning and deprecation raised while a task
runs into that task's own result, and a DYNAMIC INCLUDE's result then
goes through the strategy's "included:" path, which never displays
them - so a when: clause reading a top-level fact warns when it
SKIPS the include and stays silent when the include RUNS
(live-verified against 2.21.6 for include_tasks and include_role,
issue #974: nycrecords.nginx's passing when: ansible_os_family == 'RedHat' printed nothing while its five skipping siblings each
printed a block). Hold the readings here until the verdict is known;
#finish_deferred_fact_deprecations drops them when the include runs.
Runs block with stderr captured on the current fiber and returns what it produced; the caller decides whether/when to emit it.
Runs block with warning output captured and returns exactly the text that would have gone to STDERR (empty when nothing warned). A deprecation block raised in the window is dropped unclaimed - a caller that will replay the output wants #capture_stderr.
The Origin of a top-level fact read inside a CONDITIONAL clause
(when:/until:/...). Ansible tags every YAML scalar with its own
position and evaluates a conditional's list ITEM as one templating
operation, so a fact read inside a block-sequence clause reports that
item's scalar token (- ansible_distribution == "CentOS"), never the
when: key line (live-verified vs 2.21.6 on jetune.docker's
initialize.yml, which is the shape issue #990 caught at the key
line). A clause written on the key line itself (when: x == 'y')
keeps the plain param-VALUE position. name is the fact being read,
so the item that actually named it wins; a lone item is taken as-is
(a wrapped scalar can name the fact on a continuation line), and a
multi-item list with no naming item falls back to the key-line value
position rather than guessing which item real was evaluating.
Emits the stashed removed-param deprecations for this module unless the result says the module never ran (skip, unreachable, connection failure) or died with an uncaught exception (Ansible's crash wrapper drops the collected deprecations - live-verified vs 2.19.11 with openssl_pkcs12's maciter_size). Consumes the stash either way.
Whether a deferral scope is open on this fiber, so a caller can tell an emission that will actually reach the console from one that a running include drops (see VarSubstitutor.emit_fact_deprecation).
Display attribute diff (for file attributes like mode, owner).
Ansible's callback renders a DICT diff exactly like a content diff:
both sides are pretty-printed as sorted 4-space-indented JSON and
fed through the unified differ (live-verified vs 2.19.11 --diff:
--- before / +++ after / @@ -1,5 +1,5 @@ with the JSON lines
as +/-/context), not a per-key - key: value listing.
Display diff (delegates to specific diff types). Ansible's callback
prints a file diff BEFORE the task's status line - the diff block,
one blank line, then changed: [host] (live-verified 2.19.11) -
and starts it immediately after the TASK banner with no leading
blank of its own.
Display task result with appropriate formatting.
item_label is set for looped tasks, rendering ok: [host] => (item=x)
to match how Ansible annotates per-iteration output.
source_task carries the task's parsed source position and module
identity so a failed result can render ansible-core 2.19's
[ERROR]: Task failed: block (see ErrorBlock) before the fatal/
failed line; nil (or a task without a parsed position) suppresses
the block.
Public shape of the oneline callback's own dump (indent=0, so
separators (",", ": ")): keys sorted, no space after commas, ": "
between key and value. Used by Krikri::Oneline for the censored
no_log shape; module results go through adhoc_result_json instead
(which adds the _ansible_verbose_always indent flip on top).
Single-line sorted JSON dump at -v/-vv; Ansible's pretty 4-space-indent shape from -vvv up. Shared by every skip suffix helper.
An action-plugin warning raised while validating task args (the
service plugin's Ignoring "X" as it is not used in "systemd" for
its UNUSED_PARAMS): one console line per distinct message per run,
exactly like Ansible's Display.warning dedupe - which is global
across sources, so this shares the same set as every other
[WARNING] emitter (see emit_display_warning).
One core-side param deprecation (the module bootstrap's
removed_in_version warning, or a result's _ansible_core_deprecations
entry) on stderr - deduped per distinct text, with the one-time
"can be disabled" hint before the first one. Also called directly
from the argspec validator (a removed param deprecates when the
module's run returns - see validate's deprecation gate).
A core deprecation carrying ansible-core 2.21's Origin block: the
warning line, then Origin: <origin>, then a blank line, then -
only when payload is given - the payload line and another blank
line. Live-captured from 2.21.6 for a deprecated callback selected
from the environment: the env-origin form repeats the configured
value as a payload, while a config-file origin prints no payload.
Deduped on the whole emitted shape, like emit_core_deprecation.
Ansible's format_event ends a multi-line warning with a blank line (it appends "\n\n" when the message contains a newline, "\n" otherwise), so a multi-line [WARNING] block is followed by one blank stderr line - byte-verified against 2.19.11.
A collection-level plugin deprecation (a community.mysql -> ansible.mysql redirect, community.general.proxmox_kvm's moved-content notice): one [DEPRECATION WARNING] console block per distinct message per run, exactly like Ansible's Display. nil module names (pseudo-tasks) and non-deprecated modules are no-ops.
The deprecation counterpart of the funnel above: with a capture active the block is held back (see PendingDeprecation), otherwise it claims its dedupe key and prints - unchanged for every path that never batches.
Shared stderr funnel for the warning emitters (display warnings and the jinja-constant block alike): appends the exact would-be stderr text, newlines included, to the innermost active capture instead of writing STDERR when one is active on the current fiber.
Public so TaskExecutor's own unreachable report (report_unreachable)
can emit the same [ERROR]: Task failed: block ansible-playbook
prints before an unreachable host's fatal line - that report lives
outside ResultDisplay's result-dump path.
The failed-final-rename detail's one safe split: the atoms neither the src nor the stage path can carry ("Errno" inside a bracketed errno segment text) decide where ": [Errno N] ..." begins - rindex because the errno text itself can contain further ": b'...'" colons. Returns {segment sans errno, errno text}; nil when msg is not this shape (including a detail whose errno could not be derived, which renders plain - the same split would be a no-op anyway). Both src spellings ride real's atomic_move: the copy module's crash echoes its staged source as Python bytes (b'...'), while authorized_key echoes its /tmp mkstemp source as a plain repr ('...' - issue #819). Public only so the unit suite can pin the split directly, the same reason task_error_chain is.
The caused-by Origin of an include_vars argument-validation failure.
Ansible raises those with the OFFENDING VALUE as the exception's obj
(include_vars.py), so its event carries that value's own source
context and the chain stays two-segment instead of collapsing into
the header. Two shapes pass an obj: the unknown-option loop tags the
KEY (obj=arg - "<key> is not a valid option in include_vars",
caret on the key) and _set_args' non-list check tags the VALUE
(obj=self.valid_extensions - "The 'extensions' option must be a
list.", caret on the value). The file/dir mixing error passes the
whole args mapping (obj=self._task.args), which yields no usable
origin, so its block stays collapsed. Live-verified against
ansible-playbook 2.21.6; best-effort like the scans above, so an
option written inline (include_vars: {flie: x}) keeps the
collapsed shape rather than pointing at a wrong position.
The Origin of a top-level fact read inside a LOOP SOURCE. Ansible
renders a loop's items in order and pins the deprecation at the
list ELEMENT that named the fact - its own scalar token
(- "{{ ansible_os_family }}.yml"), never at the <key>: line -
live-verified vs 2.21.6 on mullholland.ntp's with_first_found, whose
candidates are block-sequence elements (issue #956). name is the
fact being read, so the element that actually named it wins; nil
when no element of the source names it.
The [ERROR] block that precedes a LOOPED task's per-item
ssh-unreachable failed: line (issue #275), whose shape follows the
ORIGIN host's own connection:
- origin over ssh: real prints the collapsed single segment - just the "Task failed: Failed to connect ..." brief carrying the task's Origin, no second segment, no cause section. Observed on the #275 round's real ssh-origin repro.
- ansible_connection=local origin: the two-segment chain - "Task failed." with the task Origin, then the bare ssh failure under "<<< caused by >>>". The bare ssh failure keeps its own empty events list in the chained form so collapse never folds the two segments back into one (it mirrors the ssh connection plugin's own exception event). Public only so the unit suite can pin both shapes directly (same reason task_error_chain is).
Origin context of a mapping key named key inside a task (set_fact's
variable name, include_vars' option): the key's own line/column
inside the task, found by scanning the task's source lines for the
key followed by ':' (a quoted key points at the opening quote, like
Ansible's per-key Origin).
Public so TaskExecutor's set_fact reserved-name warning (issued from
merge_ansible_facts, outside this result-dump path) can reuse the same
per-key Origin scan.
The result shape of an uncaught module exception (the plugins'
module_crash_result/unhandled_error mimicry): Ansible's module wrapper
builds that result WITHOUT the deprecations the module bootstrap
collected, so _ansible_core_deprecations riding on it must not be
displayed (live-verified vs 2.19.11 with openssl_pkcs12's
maciter_size: the crash shows no [DEPRECATION WARNING] while the
same params' fail_json and success results do).
Serializes result the way Ansible dumps a failed task's JSON: keys sorted alphabetically at every level, single line, Python's json.dumps default separators (", " between items, ": " after keys).
Python's json.dumps(obj, sort_keys=True, indent=4) plus the trailing "\n" the callback appends when it serializes dict diffs (callback/init.py _serialize_diff). The trailing newline is what keeps the unified differ from ever emitting a "\ No newline at end of file" marker for a dict diff (issue #687).
Python's repr() of a value, matching how Ansible renders a loop
item in its failed:/changed: display (True/False/None,
single-quoted strings/dict-keys, insertion-order dict {k: v}, [..]
lists) - distinct from the JSON dump used for the result object, which
stays lower-case true/false.
Replays one held-back block when its task is finally reached: the claim happens here, so an origin a dropped capture merely prepared is still free to print for the read that really happens.
Show recap of all host results, matching ansible-playbook's
v2_playbook_on_stats byte-for-byte: host column padded to 26 plain
(37 when colorized, padding applied AROUND the ANSI-wrapped name
the way Ansible's %-37s does), then " : ", then the seven counters
each shaped lead=%-4s and joined with single spaces - so every
counter carries trailing padding, including the last one.
Show the body of a content diff, built by the same algorithm ansible's
callback uses (difflib, not the system diff command): the two pick
different matching blocks for some inputs, so only difflib's hunk
layout matches ansible's stdout byte for byte (issue #736 -
martin-micimo.haproxy's haproxy.cfg was the first role where the same
34->35 line pair rendered differently).
ansible-playbook -v and above append a small JSON dump to every
"skipping:" line. A when:-false skip carries the RAW when: condition
under "false_condition" (a literal YAML false stays an unquoted
false, anything written as a string keeps its quotes); a looped
skip adds the item; a fully-skipped loop's trailing line carries
{"msg": "All items skipped"} (live-verified vs 2.19.11). Default
verbosity prints no dump at all.
A fully-skipped loop's trailing line dump. Real's shape carries "changed": false only when the per-item results themselves carried changed (every module EXCEPT debug does - a skipped debug item's displayed dump is just {false_condition, item}, live-verified vs 2.19.11 across debug/fail/set_fact/assert/file/command/include_*).
The -v dump for a when:-false skipped item of a debug: task: real shows ONLY {false_condition, item} - with the item under the CUSTOM loop_var name when loop_control sets one, plus the index_var binding when loop_control sets one - not the full cleaned skip result every other module gets (live-verified vs 2.19.11 across debug/fail/set_fact/assert/file/command/include_tasks/include_role, with and without loop_control.loop_var/index_var).
A loop whose source resolved to ZERO items skips with its own dump shape - {"skipped_reason": "No items in the list"} - distinct from both the false_condition skip and the All items skipped aggregate (live-verified vs 2.19.11).
The -v suffix for a skipped result that carries a full module payload (check-mode skips, plugin-side skipped: results): real dumps the whole cleaned result - the "skipped"/"failed" flags and the "invocation" block are absent from the visible dump at -v/-vv (invocation only shows at -vvv; live-verified vs 2.19.11). A looped skip's dump additionally carries ansible_loop_var + the item, same as ok/changed loop dumps.
The -v dump for a when:-false skipped item/task of a NON-debug module: the full cleaned skip result - changed, skip_reason, and false_condition (loop items additionally get ansible_loop_var + the item via skip_result_suffix) (live-verified vs 2.19.11).
The cause chain ansible-core 2.19 builds for each failure class, as an ErrorBlock event tree rooted at the task-level AnsibleTaskError ("Task failed."):
- template (its action plugin re-raises the loader's file-not-found
inside
exceptwithoutraise ... from): the two-segment handling chain - live-verified against 2.19.11. - fail/assert (action-level failures): "Action failed." + the result message.
- copy's controller-side src miss (its action raises
AnsibleActionFail(result=result) from exwith an empty message, so the type name becomes the middle segment): collapsed chain carrying Ansible's exact wording. - every other module-level failure: "Module failed." + the result message (the module API's own wrapper), collapsed. Public (not private) only so the unit suite can pin each module's chain shape directly, the way the other display helpers here are.
The Origin of a task param's VALUE (the position Ansible's
AnsibleActionFail obj= attaches as the failing event's source
context - e.g. add_host's groups: 5 points at the 5, column of
the value, not the key). Same best-effort text scan as
mapping_key_origin above, but the caret lands on the first
non-space character after key:.
Public (same reason as mapping_key_origin) so the lookup dispatch's
Jinja-const embedded-template warning - emitted from inside the
expression evaluator while THIS param is being finalized, and the
evaluator otherwise has no route to the task (issue #259) - can
produce the same value-position Origin real's warning carries.
When the caller already carries just the param's source position
(the evaluator, which can't name Task at all - see
VarSubstitutor#current_task_param), use param_value_origin.
Update stats based on task result. A failure with ignore_errors: yes still displays as failed (see display_result) but doesn't count toward the host's failure tally - matching Ansible, where an ignored failure doesn't fail the play or the process exit code.