diff --git a/commands/bootstrap-tenant.sh b/commands/bootstrap-tenant.sh index 23a160d..ee251c6 100755 --- a/commands/bootstrap-tenant.sh +++ b/commands/bootstrap-tenant.sh @@ -233,6 +233,27 @@ append_line_once() { chown "$TENANT_USER:$TENANT_GROUP" "$file" } +# The binary on PATH is not the effective state — an image can ship crontab +# with cron.service masked or stopped, and an unarmed timer is exactly the +# silent-inert box #162 is about. Converge best-effort, then assert what +# systemd actually reports; the assert is the authority. Enabling an +# already-enabled unit is a no-op and no path here touches any crontab. +# A function so test/cli.sh can lift it verbatim and drive it against a +# stubbed systemctl, the drop_incus precedent. +converge_cron() { + if ! systemctl is-enabled cron >/dev/null 2>&1; then + log "cron.service not enabled — converging" + systemctl unmask cron >/dev/null 2>&1 || true + systemctl enable cron >/dev/null 2>&1 || true + fi + if ! systemctl is-active cron >/dev/null 2>&1; then + log "cron.service not active — starting" + systemctl start cron >/dev/null 2>&1 || true + fi + systemctl is-enabled cron >/dev/null 2>&1 || die "cron.service is not enabled after converge — the duty engine's timer never fires without it (#162)" + systemctl is-active cron >/dev/null 2>&1 || die "cron.service is not active after converge — the duty engine's timer never fires without it (#162)" +} + # --- packages ---------------------------------------------------------------- export DEBIAN_FRONTEND=noninteractive log "installing base packages (tenant ${ROLE})" @@ -260,23 +281,8 @@ if [ "$ROLE" != "staging-box" ]; then command -v gh >/dev/null 2>&1 || die "gh missing after package install" command -v git >/dev/null 2>&1 || die "git missing after package install" command -v crontab >/dev/null 2>&1 || die "crontab missing after package install — the duty engine arms itself with cron (#162)" - # The binary on PATH is not the effective state — an image can ship crontab - # with cron.service masked or stopped, and an unarmed timer is exactly the - # silent-inert box #162 is about. Converge best-effort, then assert what - # systemd actually reports; the assert is the authority. Enabling an - # already-enabled unit is a no-op and no path here touches any crontab. # staging-box is exempt with the rest of this block: no agent, no engine. - if ! systemctl is-enabled cron >/dev/null 2>&1; then - systemctl unmask cron >/dev/null 2>&1 || true - systemctl enable cron >/dev/null 2>&1 || true - log "enabled cron.service" - fi - if ! systemctl is-active cron >/dev/null 2>&1; then - systemctl start cron >/dev/null 2>&1 || true - log "started cron.service" - fi - systemctl is-enabled cron >/dev/null 2>&1 || die "cron.service is not enabled after converge — the duty engine's timer never fires without it (#162)" - systemctl is-active cron >/dev/null 2>&1 || die "cron.service is not active after converge — the duty engine's timer never fires without it (#162)" + converge_cron fi # --- docker ------------------------------------------------------------------ diff --git a/test/cli.sh b/test/cli.sh index 48cf3da..d5b59ab 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -667,6 +667,97 @@ check "tenant: cron.service enabled assert is present" 0 "" \ grep -qF "systemctl is-enabled cron" "$ROOT/commands/bootstrap-tenant.sh" check "tenant: cron.service active assert is present" 0 "" \ grep -qF "systemctl is-active cron" "$ROOT/commands/bootstrap-tenant.sh" + +# The converge is exercised, not argued about (the drop_incus precedent): +# converge_cron is lifted out of the real file verbatim — column-0 +# 'converge_cron() {' through column-0 '}' — and driven against a stub +# systemctl whose effective state lives in files. The extraction is asserted +# first: if that shape ever changes the lift comes back empty and every case +# below fails loudly rather than passing vacuously. +CRON_FN="$(sed -n '/^converge_cron() {/,/^}/p' "$ROOT/commands/bootstrap-tenant.sh")" +# shellcheck disable=SC2016 # $1 is the inner bash -c's positional, deliberately +check "tenant: converge_cron lifts out of the real file whole" 0 "" \ + bash -c '[ -n "$1" ] && printf %s "$1" | grep -q "^}$"' _ "$CRON_FN" +# ...and the function must actually be CALLED — a lifted-and-driven function +# nobody invokes proves nothing about bootstrap. +check "tenant: converge_cron is invoked" 0 "" \ + grep -qE '^ *converge_cron$' "$ROOT/commands/bootstrap-tenant.sh" + +CRON_BASH="$(command -v bash)" +# drive_cron — the real converge_cron against +# a stub systemctl. Effective state is files: 'enabled'/'active' existing means +# the probe passes. 'noop': both preexist — the idempotent re-run. 'converge': +# neither, and enable/start take effect. 'masked': neither, and enable/start do +# NOTHING — the unrecoverably-inert daemon #162 is about. 'deadstart': enabled, +# but start never takes. The stub logs its calls to a file: the real call sites +# are '>/dev/null 2>&1', so a stub that spoke on either stream would be +# silenced and the call assertions below would pass vacuously. +drive_cron() { + local mode="$1" d + d="$(mktemp -d)" + mkdir -p "$d/bin" + case "$mode" in noop) : > "$d/enabled"; : > "$d/active" ;; deadstart) : > "$d/enabled" ;; esac + # The stub restores a real PATH for itself: the caller's PATH is REPLACED by + # the stub dir (that is what keeps a host systemctl out of reach), which + # would otherwise leave the stub unable to find 'echo' as an executable. + cat > "$d/bin/systemctl" <> "$d/calls" +case "\$1" in + is-enabled) [ -e "$d/enabled" ] ;; + is-active) [ -e "$d/active" ] ;; + enable) case "$mode" in noop|converge) : > "$d/enabled" ;; esac ;; + start) case "$mode" in noop|converge) : > "$d/active" ;; esac ;; +esac +EOF + chmod +x "$d/bin/systemctl" + # The driving shell mirrors the real script: same set flags, same log/die. + # shellcheck disable=SC2016 # $*/$1/$2 resolve inside the driving shell + PATH="$d/bin" "$CRON_BASH" -c ' + set -euo pipefail + log() { printf "rig-bootstrap: %s\n" "$*"; } + die() { printf "rig-bootstrap: ERROR: %s\n" "$1" >&2; exit "${2:-1}"; } + '"$CRON_FN"' + converge_cron' 2>&1 + echo "RC=$?" + cat "$d/calls" 2>/dev/null + rm -rf "$d" +} +CRON_NOOP="$(drive_cron noop)" +CRON_CONV="$(drive_cron converge)" +CRON_MASK="$(drive_cron masked)" +CRON_DEAD="$(drive_cron deadstart)" +cron_has() { printf '%s' "$1" | grep -qF -e "$2"; } # cron_has + +# The idempotent re-run: both probes already pass, NOTHING is converged and +# nothing dies — a second bootstrap must not touch the unit. +check "converge_cron: already enabled+active exits 0" 0 "" cron_has "$CRON_NOOP" "RC=0" +check "converge_cron: the no-op never calls unmask" 1 "" cron_has "$CRON_NOOP" "systemctl unmask" +check "converge_cron: the no-op never calls enable" 1 "" cron_has "$CRON_NOOP" "systemctl enable" +check "converge_cron: the no-op never calls start" 1 "" cron_has "$CRON_NOOP" "systemctl start" +# The converge path: a disabled, stopped unit is unmasked, enabled, started — +# and the asserts then pass on systemd's own answer, exit 0. +check "converge_cron: disabled+inactive converges, exits 0" 0 "" cron_has "$CRON_CONV" "RC=0" +check "converge_cron: the converge unmasks" 0 "" cron_has "$CRON_CONV" "systemctl unmask cron" +check "converge_cron: the converge enables" 0 "" cron_has "$CRON_CONV" "systemctl enable cron" +check "converge_cron: the converge starts" 0 "" cron_has "$CRON_CONV" "systemctl start cron" +# The log states the probe fact, never a success it did not verify. +check "converge_cron: the log states the probe fact" 0 "" \ + cron_has "$CRON_CONV" "cron.service not enabled — converging" +# THE #162 FAILURE: a converge that does not take effect DIES, nonzero, naming +# cron — never a silent success wrapping an inert timer. +check "converge_cron: an unrecoverable unit dies nonzero" 0 "" cron_has "$CRON_MASK" "RC=1" +check "converge_cron: the death names the enabled assert" 0 "" \ + cron_has "$CRON_MASK" "cron.service is not enabled after converge" +check "converge_cron: the death cites #162" 0 "" cron_has "$CRON_MASK" "#162" +check "converge_cron: the dying path tried to converge first" 0 "" \ + cron_has "$CRON_MASK" "systemctl unmask cron" +# Enabled but the start never takes: the ACTIVE assert dies — enabled alone +# is not an armed timer. +check "converge_cron: enabled-but-dead start dies nonzero" 0 "" cron_has "$CRON_DEAD" "RC=1" +check "converge_cron: that death names the active assert" 0 "" \ + cron_has "$CRON_DEAD" "cron.service is not active after converge" # The machine-role traits die with the tenant story, never "unknown flag" — an # operator coming from the machine families needs the boundary, not a shrug. check "tenant: trait flags die with the tenant story" 2 "have no traits" \