feat(reconcile): re-assert pool membership after restore-over-existing (campaign-2 R2, v0.74.0)
Pool membership is what lets the pool-scoped token reach a guest; pct restore --pool sets it only at CREATE, so a restore over an existing VMID drops the guest from the felhom pool and 403s the next restore-test/DR on VM.Audit. This empty-pool state is the true root cause of the campaign's "R1" (bind-mount restore failing was a symptom — restore-test's existing bind neutralization never ran without config-read). Add Client.PoolAddVMID (PUT /pools, additive+idempotent, Pool.Allocate) and call it in bring-up after liveness when spec.Pool!="" — warn-not-fail on a hiccup (liveness wins). B3 scratch-teardown 403 diagnosed as a cascade (restoretest already passes Pool). Role/ACL untouched. Tests + red-proof. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -629,3 +629,91 @@ func lockBackoffFast(t *testing.T) {
|
||||
configLockBackoff = time.Millisecond
|
||||
t.Cleanup(func() { configLockBackoff = prev })
|
||||
}
|
||||
|
||||
// --- Part B (campaign-2 R2): pool re-assertion after bring-up ------------------------------------
|
||||
|
||||
// poolAddsFor returns the recorded PoolAddVMID calls for a vmid.
|
||||
func poolAddsFor(api *fakeAPI, vmid int) []poolAddCall {
|
||||
var out []poolAddCall
|
||||
for _, c := range api.poolAdds {
|
||||
if c.vmid == vmid {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// A bring-up with a Pool set MUST re-assert pool membership after the restore (so a
|
||||
// restore-over-existing that dropped membership is healed for the NEXT restore-test/DR).
|
||||
// COMPANION RED-PROOF: on the pre-fix code (no PoolAddVMID call) poolAddsFor is empty → this FAILS.
|
||||
func TestRunBringUp_ReassertsPoolMembership(t *testing.T) {
|
||||
const vmid = 8100
|
||||
api := &fakeAPI{cfg: map[int]proxmox.GuestConfig{vmid: scratchCfg()}}
|
||||
e, _, q := newEngine(t, api, EmptyProvider{})
|
||||
defer q.Close()
|
||||
|
||||
res := e.RunBringUp(context.Background(), BringUpSpec{
|
||||
Mode: ModeDRGuestLoss, Archive: "local:backup/cust.tar.zst", VMID: vmid,
|
||||
RestoreStorage: "local-lvm", Pool: "felhom",
|
||||
})
|
||||
if res.Err != nil || !res.Pass {
|
||||
t.Fatalf("bring-up must pass, got %+v", res)
|
||||
}
|
||||
adds := poolAddsFor(api, vmid)
|
||||
if len(adds) != 1 || adds[0].pool != "felhom" {
|
||||
t.Fatalf("pool membership not re-asserted: poolAdds=%+v", api.poolAdds)
|
||||
}
|
||||
}
|
||||
|
||||
// No Pool → no PoolAddVMID call (a broad-token restore needs no pool).
|
||||
func TestRunBringUp_NoPool_NoPoolAdd(t *testing.T) {
|
||||
const vmid = 8101
|
||||
api := &fakeAPI{cfg: map[int]proxmox.GuestConfig{vmid: scratchCfg()}}
|
||||
e, _, q := newEngine(t, api, EmptyProvider{})
|
||||
defer q.Close()
|
||||
|
||||
res := e.RunBringUp(context.Background(), BringUpSpec{
|
||||
Mode: ModeProvision, Archive: "local:backup/golden.tar.zst", VMID: vmid,
|
||||
RestoreStorage: "local-lvm", Hostname: "h",
|
||||
})
|
||||
if res.Err != nil || !res.Pass {
|
||||
t.Fatalf("bring-up must pass, got %+v", res)
|
||||
}
|
||||
if len(api.poolAdds) != 0 {
|
||||
t.Fatalf("no Pool set → PoolAddVMID must not be called: %+v", api.poolAdds)
|
||||
}
|
||||
}
|
||||
|
||||
// A pool-add FAILURE must NOT flip a healthy, running guest's verdict — it surfaces as a LOUD warning
|
||||
// (membership matters for the NEXT op, not this guest's boot). Liveness wins.
|
||||
func TestRunBringUp_PoolAddFailure_WarnsButPasses(t *testing.T) {
|
||||
const vmid = 8102
|
||||
api := &fakeAPI{
|
||||
cfg: map[int]proxmox.GuestConfig{vmid: scratchCfg()},
|
||||
poolAddErr: errors.New("proxmox: PUT /pools/felhom -> HTTP 500: transient"),
|
||||
}
|
||||
e, _, q := newEngine(t, api, EmptyProvider{})
|
||||
defer q.Close()
|
||||
|
||||
res := e.RunBringUp(context.Background(), BringUpSpec{
|
||||
Mode: ModeDRGuestLoss, Archive: "local:backup/cust.tar.zst", VMID: vmid,
|
||||
RestoreStorage: "local-lvm", Pool: "felhom",
|
||||
})
|
||||
if res.Err != nil || !res.Pass {
|
||||
t.Fatalf("a pool-add hiccup must NOT fail a healthy guest, got %+v", res)
|
||||
}
|
||||
// the guest is KEPT (not torn down) despite the pool-add failure.
|
||||
if len(api.destroys) != 0 {
|
||||
t.Fatalf("pool-add failure must not trigger teardown: %+v", api.destroys)
|
||||
}
|
||||
// the failure is surfaced as a warning.
|
||||
found := false
|
||||
for _, w := range res.StartWarnings {
|
||||
if strings.Contains(w, "pool re-assert failed") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("pool-add failure must surface as a warning: %+v", res.StartWarnings)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user