runs: add non-destructive flag + operator Cancel button
CI / Lint + build + test (push) Successful in 2m5s
Release / release (push) Successful in 3m5s

Non-destructive pre-declares "don't touch the disks" on Start: the
Storage stage skips wipe-probe, badblocks -w, and write-mode fio,
and reports a read-only summary. Runs a new non_destructive column;
threaded through Claim → agent tests.Deps → Storage stage.

Cancel halts an in-flight run. The orchestrator transitions to a
new StateCancelled via TriggerOperatorCancelled (valid from any
active state); the agent's next heartbeat returns cmd=cancel_stage,
which fires a stored CancelFunc on the per-stage context. Stage
subprocesses spawned with exec.CommandContext die with the context,
the agent posts a cancelled outcome, then powers the host off.

Destructive stages mid-run may leave the host in an intermediate
state — the UI confirm dialog warns the operator; recovery is
manual for now.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 13:01:42 -04:00
parent 2c440fce8a
commit 4524ab8dc0
22 changed files with 434 additions and 230 deletions
+17
View File
@@ -44,6 +44,23 @@ func Storage(ctx context.Context, d Deps) Outcome {
}
}
// Non-destructive runs skip wipe-probe (nothing to refuse), badblocks
// -w, and write-mode fio. Every expected disk is still asserted
// present + readable by listing /sys/block and reading SMART-accessible
// identity; the per-disk map flags the shortcut so the report is clear.
if d.NonDestructive {
perDisk := map[string]any{}
for _, t := range targets {
perDisk[t.Device] = map[string]any{"mode": "non_destructive", "serial": t.Serial}
}
d.Info(fmt.Sprintf("Storage: non-destructive — verified %d disk(s) present", len(targets)))
return Outcome{
Passed: true,
Summary: fmt.Sprintf("non-destructive: read-only checks only (%d disks)", len(targets)),
Extras: map[string]any{"per_disk": perDisk, "non_destructive": true},
}
}
// Wipe probe on every target. A single dirty disk halts the stage
// unless the operator has set OverrideWipe via the UI.
probes := map[string]wipeProbeResult{}