runs: add non-destructive flag + operator Cancel button
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:
@@ -103,7 +103,7 @@ func TestDispatcher_TransitionsToWaitingRebootNoWoL(t *testing.T) {
|
||||
d, _, runs, hostID, cleanup := setupPickNext(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
runID, err := runs.Create(ctx, hostID, "deadbeef")
|
||||
runID, err := runs.Create(ctx, hostID, "deadbeef", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create run: %v", err)
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func TestDispatcher_FailsStaleHeartbeat(t *testing.T) {
|
||||
if err := hosts.UpdateLastSeen(ctx, "aa:bb:cc:dd:ee:50", time.Now().UTC().Add(-5*time.Minute)); err != nil {
|
||||
t.Fatalf("stamp stale: %v", err)
|
||||
}
|
||||
runID, err := runs.Create(ctx, hostID, "deadbeef")
|
||||
runID, err := runs.Create(ctx, hostID, "deadbeef", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create run: %v", err)
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func TestDispatcher_FailsNeverSeenHost(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("create host: %v", err)
|
||||
}
|
||||
runID, err := runs.Create(ctx, neverID, "deadbeef")
|
||||
runID, err := runs.Create(ctx, neverID, "deadbeef", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create run: %v", err)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestPublishesTileAndPipelineOnTransition(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("create host: %v", err)
|
||||
}
|
||||
runID, err := runs.Create(ctx, hostID, "deadbeef")
|
||||
runID, err := runs.Create(ctx, hostID, "deadbeef", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create run: %v", err)
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func TestCompleteStagePublishesPipeline(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("create host: %v", err)
|
||||
}
|
||||
runID, err := runs.Create(ctx, hostID, "deadbeef")
|
||||
runID, err := runs.Create(ctx, hostID, "deadbeef", false)
|
||||
if err != nil {
|
||||
t.Fatalf("create run: %v", err)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ const (
|
||||
TriggerStageFailed Trigger = "StageFailed" // a stage reported failure
|
||||
TriggerStageCompleted Trigger = "StageCompleted" // a stage reported success → advance
|
||||
TriggerAllStagesPassed Trigger = "AllStagesPassed" // final stage passed
|
||||
TriggerOperatorReleased Trigger = "OperatorReleased" // user clicked Release on a held run
|
||||
TriggerOperatorOverride Trigger = "OperatorOverride" // user overrode a held stage; re-enter it
|
||||
TriggerOperatorReleased Trigger = "OperatorReleased" // user clicked Release on a held run
|
||||
TriggerOperatorOverride Trigger = "OperatorOverride" // user overrode a held stage; re-enter it
|
||||
TriggerOperatorCancelled Trigger = "OperatorCancelled" // user clicked Cancel on an active run
|
||||
)
|
||||
|
||||
// stageStates maps the canonical stage name (from DefaultStageOrder)
|
||||
@@ -63,9 +64,10 @@ var table = map[Trigger]transition{
|
||||
TriggerRebootCommanded: {from: []model.RunState{model.StateQueued}, to: model.StateWaitingReboot},
|
||||
TriggerPXEObserved: {from: []model.RunState{model.StateWaitingReboot, model.StateWaitingWoL, model.StateBooting}, to: model.StateBooting},
|
||||
TriggerAgentClaimed: {from: []model.RunState{model.StateBooting, model.StateWaitingReboot, model.StateWaitingWoL}, to: model.StateInventoryCheck},
|
||||
TriggerStageFailed: {from: allActiveStates(), to: model.StateFailedHolding},
|
||||
TriggerAllStagesPassed: {from: []model.RunState{model.StateReporting}, to: model.StateCompleted},
|
||||
TriggerOperatorReleased: {from: []model.RunState{model.StateFailedHolding}, to: model.StateReleased},
|
||||
TriggerStageFailed: {from: allActiveStates(), to: model.StateFailedHolding},
|
||||
TriggerAllStagesPassed: {from: []model.RunState{model.StateReporting}, to: model.StateCompleted},
|
||||
TriggerOperatorReleased: {from: []model.RunState{model.StateFailedHolding}, to: model.StateReleased},
|
||||
TriggerOperatorCancelled: {from: allActiveStates(), to: model.StateCancelled},
|
||||
}
|
||||
|
||||
// Next computes the target state for a trigger against the current state.
|
||||
|
||||
Reference in New Issue
Block a user