module

Krikri::BatchScript

Wire protocol for task batching (on by default; --no-batching disables it): builds the bash script that runs several plugin invocations in one SSH round trip, and parses the results back out.

Both directions use base64 framing, never marker strings mixed into a plugin's own stdout/stderr - a shell/command task's captured output could in principle contain anything, including a string that collides with a hand-picked delimiter. Base64's own alphabet structurally can't produce that collision, so this is immune to it rather than merely unlikely to hit it.

The generated script is meant to be piped via a single ssh ... bash -s invocation's stdin. Each step's config is embedded in the script itself (also base64) rather than streamed separately over that same stdin - interleaving "script source" and "step data" on one stdin stream is a well-known footgun (bash's own script-reading can read ahead in blocks, not strictly line-by-line, corrupting the boundary between where the script ends and data begins). A single self-contained script sidesteps that entirely.

Assumes GNU coreutils base64 (supports -w0, no line wrapping) on the target - consistent with this project's existing assumption of a Debian/RHEL-family Linux target elsewhere (dpkg/rpm detection, etc.).

Constants

REMOTE_DIR_PREFIX = "/var/tmp/.krikri-playbook/batch-"

/var/tmp, not /tmp - see PluginManager.remote_plugin_dir for why (some hardening roles remount /tmp as a fresh, empty tmpfs mid-play, which would silently wipe this batch's own per-step output files out from under it).

TOP_LEVEL_FAILED_AWK = "{\n line = $0\n depth = 0\n in_str = 0\n prev = \"\"\n failed = 0\n bs_run = 0\n n = length(line)\n q = sprintf(\"%c\", 34)\n bs = sprintf(\"%c\", 92)\n key = q \"failed\" q \":\"\n for (i = 1; i <= n; i++) {\n c = substr(line, i, 1)\n if (in_str) {\n if (c == bs) {\n bs_run++\n } else {\n # a quote closes the string only when the run of\n # immediately-preceding backslashes is even (an odd run\n # escapes it: \"x\" ends with an escaped backslash, so the\n # quote is the real terminator)\n if (c == q && bs_run % 2 == 0) in_str = 0\n bs_run = 0\n }\n } else if (depth == 1 && substr(line, i, 9) == key && substr(line, i + 9) ~ /^ *true/) {\n failed = 1\n } else if (c == q) {\n in_str = 1\n } else if (c == \"{\" || c == \"[\") {\n depth++\n } else if (c == \"}\" || c == \"]\") {\n depth--\n }\n prev = c\n }\n exit (failed ? 0 : 1)\n}"

awk program (POSIX awk, run against one step's stdout file) that answers "does this result JSON have a TOP-LEVEL "failed": true?"

  • exits 0 when it does. Tracks JSON string/escape state and brace depth so "failed" keys nested inside result values (uri:'s parsed response body, string-valued fields carrying escaped copies) never trip it. The result is always a single-line compact JSON object, so per-line state resets are harmless.

Class methods

build(batch_id : String, steps : Array(Step)) : String

Builds the full script for one batch. batch_id should be unique per invocation (avoids any risk of colliding with a leftover directory from an earlier batch against the same host).

Source
parse(raw_stdout : String) : Hash(Int32, StepResult)

Parses a batch script's stdout (as captured from the single SSH invocation that ran it) back into per-step results, keyed by the step's original index. A step whose index is missing never ran (the script halted before reaching it, or the whole batch never started)

  • callers must not treat a missing entry as an error on its own.
Source

Nested types