Host power-off: re-check jobs before each step; cancel if one is still running (R-998)
gates / gates (push) Successful in 29s
gates / gates (push) Successful in 29s
Security review of f38b03c: a controller update, drive/NAS job, escrow ceremony or Proxmox
vzdump could start between acceptance and the power-off (the heavy gate does not cover them).
runHostPowerOff now asks jobBlocker again before the guest shutdown and before the host
power-off, waits up to 15 min, and CANCELS (gate released, box stays on) if still busy.
Red-proofed: no wait, and a gate kept after a cancel, each convicted.
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -280,3 +281,50 @@ func TestRunningGuestsFromProc(t *testing.T) {
|
||||
t.Fatal("an unreadable /proc must be an error")
|
||||
}
|
||||
}
|
||||
|
||||
// switchVzdump is a vzdump reader the test can flip while the power-off runs.
|
||||
type switchVzdump struct {
|
||||
fakeVzdump
|
||||
on *atomic.Bool
|
||||
}
|
||||
|
||||
func (f switchVzdump) BackupRunning(context.Context, int) (bool, error) { return f.on.Load(), nil }
|
||||
|
||||
// A job that starts AFTER acceptance (security review 2026-10-13): the power-off waits; if the job is still running
|
||||
// when the wait ends, the power-off is cancelled — no guest shutdown, no host power-off, the gate given back.
|
||||
func TestHostPowerOff_JobStartedAfterAcceptance(t *testing.T) {
|
||||
// (a) it clears during the wait → the power-off goes ahead after it
|
||||
r := newPowerRig(t, time.Hour)
|
||||
var on atomic.Bool
|
||||
r.srv.staleLock = switchVzdump{on: &on}
|
||||
r.srv.powerOff.JobWait, r.srv.powerOff.JobPoll = 2*time.Second, 2*time.Millisecond
|
||||
r.srv.powerOff.ReportNow = func(context.Context) bool { on.Store(true); return true } // a vzdump starts now
|
||||
if w := do(t, r.h, "POST", "/host/poweroff", "A", ""); w.Code != http.StatusAccepted {
|
||||
t.Fatalf("accept: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
if s := r.power.got(); len(s) != 0 {
|
||||
t.Fatalf("a step ran while the vzdump ran: %v", s)
|
||||
}
|
||||
on.Store(false)
|
||||
if s := r.waitSteps(t, 2); len(s) != 2 {
|
||||
t.Fatalf("after the vzdump ended the steps = %v, want shutdown + poweroff", s)
|
||||
}
|
||||
|
||||
// (b) it never clears → cancelled, nothing switched off, the heavy gate free again
|
||||
r2 := newPowerRig(t, time.Hour)
|
||||
var on2 atomic.Bool
|
||||
r2.srv.staleLock = switchVzdump{on: &on2}
|
||||
r2.srv.powerOff.JobWait, r2.srv.powerOff.JobPoll = 40*time.Millisecond, 2*time.Millisecond
|
||||
r2.srv.powerOff.ReportNow = func(context.Context) bool { on2.Store(true); return true }
|
||||
if w := do(t, r2.h, "POST", "/host/poweroff", "A", ""); w.Code != http.StatusAccepted {
|
||||
t.Fatalf("accept: %d", w.Code)
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if s := r2.power.got(); len(s) != 0 {
|
||||
t.Fatalf("cancelled power-off still ran: %v", s)
|
||||
}
|
||||
if busy := r2.srv.inFlight.Busy(); busy != "" {
|
||||
t.Fatalf("the heavy gate is still held after a cancel: %q", busy)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user