Krikri::SSHManager
Constants
Default per-command execution timeout for #exec/#exec_script/
#daemon_send. Real Ansible has NO default command-duration limit
at all - a foreground task runs until it completes, however long
that takes (only async: tasks get an explicit max duration);
what actually detects a genuinely dead/unreachable host is the SSH
connection's own keepalive (ServerAliveInterval=60 x
ServerAliveCountMax=3 below, ~180s to a hard disconnect), which
this timeout duplicates and undercuts. Previously 300s (5 minutes)
- too short for entirely ordinary, legitimately slow real-world
tasks: found live re-benchmarking buluma.netdata (round 163
regression check) - its own installer genuinely compiles from
source and took a confirmed 1536s (~25.6 minutes) on the real
ansible-playbook side; crystal's identical task was killed at
exactly 300s ("SSH command timed out... did not exit even after
being killed") despite the remote command still actively running
and eventually would have succeeded. The same 300s cap likely also
explains 2 earlier "flaky"-looking incidents this session
(robertdebock.luks, geerlingguy.java - both ordinary
dnf installcalls that occasionally took a bit over 300s on a slow mirror/cold host, not a genuine hang) that were chased down as one-off infra flakiness via isolated re-tests rather than recognized as this same root cause at the time. Raised to a generous but still-bounded 1 hour - long enough for realistic compile-from-source/large- package-install tasks, while the SSH keepalive above still catches a truly dead connection in ~3 minutes regardless of this value.
Class methods
Graceful shutdown for every still-open daemon connection, called
once at the end of a run. Closes every daemon's stdin first (each
one sees EOF and exits cleanly via plugin_daemon.cr's own
rescue IO::EOFError - no explicit "goodbye" message needed),
THEN waits once for the whole batch rather than once per
connection, force-killing stragglers only after that single grace
period - materially faster than #kill_daemon's per-connection
path would be for a multi-host run.
Close specific connection
Exception counterpart of #connection_level_failure? - an scp/rsync/ ssh failure surfaces as a raised "Failed to upload/download ..." whose detail embeds ssh's own stderr, so the same pattern list decides whether the exception means "the transport never got there" (UNREACHABLE in real Ansible) or something else that must keep propagating (a missing local file, a staging-dir safety refusal, an engine bug). Message-only, no exit code to gate on.
True when exit_code/stderr look like the SSH transport itself failing - ssh never reached or never authenticated to the host - as opposed to a remote plugin crashing after a successful connection. Public so PluginManager's remote-result interpretation and the spec suite share one pattern list (see CONNECTION_FAILURE_PATTERNS for what deliberately does NOT match).
Read-only accessor for diagnostics/specs - the stable control-socket directory itself carries no secrets, and the spec suite pins its pid-independence (the exact property f500.ufw's warm-rerun lockout hinged on).
Sends one request and returns the plugin's own JSON result,
unwrapped - unlike exec/exec_script, there is no exit-code-vs-
stdout arbitration to do here (that was a one-shot-process
concept; interpret_remote_result doesn't apply to a persistent
pipe), the daemon's response IS the plugin's real output.
On ANY failure (spawn error, broken pipe, timeout, malformed
response) the connection is torn down and NOT retried here - the
exception propagates to PluginManager, whose job is to catch it
and fall back to execute_remote_plugin's existing, already-
proven per-task path for that one call. This is deliberately the
WHOLE reconnect story: a stale daemon (e.g. after ansible. builtin.reboot killed the SSH session mid-play) fails exactly
once, falls back safely for that one task, and a fresh daemon gets
lazily spawned the next time this host needs one - no explicit
reboot-awareness needed anywhere in this method.
Perf item 3: send a whole batch of steps as ONE daemon request. Same connection, same framing and the same rescue-and-let-the-caller-fall-back contract as #daemon_send above; only the payload shape differs.
Returns the plugin results keyed by the step's index in steps. An
index that is ABSENT never ran - the daemon stopped at an earlier
failing step, exactly as BatchScript.parse reports a script that
halted. Callers must honour that the same way they already do for
the script transport.
Every step here must agree on become_user, because that is what selects which resident daemon (and therefore which user) serves them - see TaskExecutor#run_batch_steps, which does the partitioning.
Download file from remote host via SCP
Execute command on remote host port nil means "let ssh resolve it" (no -p flag; ~/.ssh/config and /etc/ssh/ssh_config apply) - only an explicit port overrides.
Runs script on the remote host via ssh ... bash -s, piped over
that single invocation's own stdin - used by TaskExecutor's batch
path (batching, on by default; --no-batching disables it) to run
several plugin invocations
in one SSH round trip. Deliberately separate from exec: exec
wraps a command through bash -c <string>, which has no stdin of
its own to carry a whole script through; this uses bash -s
(reads its script from stdin) specifically so the caller can hand
over a multi-step script without needing to escape it into a
single command-line string.
meta: reset_connection - drops this host's persistent connection
state so the next task opens fresh connections. Two layers exist
here: the resident plugin daemons (one ssh process per
host/user/port/become_user) and ssh's own ControlMaster socket
underneath them. Both are dropped for every key matching the host;
the ssh -O exit is best-effort (no master socket = no-op).
Upload file using rsync (more efficient for incremental updates) Returns true if successful, false if rsync not available or failed
Batch upload multiple files using rsync (most efficient) Returns true if successful, false if rsync not available or failed
Runs block (given the just-spawned process) on a separate fiber
and bounds it to timeout_seconds wall-clock time - both exec
and exec_script accepted a timeout: parameter that was never
actually enforced anywhere, leaving every blocking Process call
(#wait, and for exec_script, the write of a potentially large
script into the process's own stdin pipe) able to hang forever
with no escape hatch at all. -o ServerAliveInterval=/
ServerAliveCountMax= only bound an established SSH session
going idle - they do nothing for a local pipe write blocking
because nothing is reading the other end (the remote host went
unreachable mid-handshake, before ssh itself even started
forwarding stdin), which is a local blocking syscall the SSH
protocol's own keepalive machinery never sees. Found running
konstruktoid-hardening's newly-batched (0.9.155) 411-item "Find
possible suid binaries" step: the whole krikri-playbook process
sat silent, producing no further output and no error, until an
external timeout wrapper eventually killed it - a real host/
network hang, not a task bug, and previously nothing inside
krikri-playbook itself could ever recover from it.
On timeout, SIGKILLs the process and gives it a further 5s to actually exit and report a status before giving up entirely (a hard bound of its own, in case even the kill signal doesn't unblock the fiber - e.g. if it's wedged in kernel-level uninter- ruptible I/O, vanishingly rare but not impossible).
Fiber lifecycle on the timeout path (audited, not assumed): the
SIGKILL guarantees the child dies, which unblocks the spawned
fiber's #wait (or errors its stdin pipe write with EPIPE), so
the fiber completes and its result lands in the capacity-1
channel - either received within the 5s grace (the killed
process's real exit status is then returned, e.g. 137) or, if
even that doesn't arrive in time, discarded when this method
returns and the channel is garbage. Either way the fiber
terminates on its own; nothing leaks permanently.
Public (not private) so the timeout semantics can be spec'd directly against a real local process - see spec/unit/ssh_manager_timeout_spec.cr.