agent v0.33.0: C1 net — pre-start self-heal hook + decommission mp-delete

Pre-start PVE hookscript (internal/guesthook) creates host-root placeholders for
absent bind-mount sources so the guest always boots (fail-closed); decommission
now pct set --delete's the dead mp (GuestBinder.DetachBind) so a missing source
can't brick the next reboot (B3 C1 bug). Non-hollow tests + companions. Installed
+ registered per-guest by the provision back-half. Transitional ahead of the
intermediary-mount re-architecture which makes C1 structural.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:11:10 +02:00
parent 2a4affc3a8
commit 44cdf82631
11 changed files with 490 additions and 3 deletions
+30
View File
@@ -124,6 +124,36 @@ func TestDecommission_Effects(t *testing.T) {
}
}
// TestDecommission_DeletesGuestMount is the C1-fix regression: decommission must `--delete` the guest
// mountpoint slot that binds the drive, so its now-missing source can't brick the next boot. The guest
// has two binds (bootstrap mp9 + the data drive mp1); only mp1 (the one targeting /mnt/bulk) may be
// detached.
//
// COMPANION GUARD: the pre-fix handler (the B3 bug) never called DetachBind → detachCount()==0 → this
// test FAILS on it. A trivial impl deleting the WRONG/first slot is caught by the slot==mp1 assertion.
func TestDecommission_DeletesGuestMount(t *testing.T) {
d := &fakeDiskOps{}
intent := newFakeIntent()
intent.SetEnrolled("uuid:usb-1")
ga := &fakeGuestAttacher{}
srv := decommServer(t, d, userDataAndProtected(), fakeGuestList{}, intent, tempBindStore(t), ga, map[int]map[string]string{
8200: {
"mp9": "/var/lib/.../bootstrap,mp=/etc/felhom-bootstrap,ro=1",
"mp1": "/mnt/bulk/felhom-data,mp=/mnt/bulk",
},
})
w := do(t, srv.Handler(), "POST", "/disks/decommission", "A", `{"where":"/mnt/bulk"}`)
if w.Code != http.StatusOK {
t.Fatalf("decommission: got %d want 200 (%s)", w.Code, w.Body.String())
}
if ga.detachCount() != 1 {
t.Fatalf("DetachBind called %d times, want 1 (C1 fix: the dead mp must be deleted)", ga.detachCount())
}
if got := ga.detaches[0]; got.vmid != 8200 || got.slot != "mp1" {
t.Fatalf("DetachBind(vmid=%d, slot=%q), want (8200, mp1) — wrong slot deleted", got.vmid, got.slot)
}
}
// TestReassertGuestBinds_SkipsDecommissioned is the load-bearing F9-reconnect invariant: a
// decommissioned-but-present drive still recorded in the bind store must NOT auto-rebind on agent
// restart. Companion: with intent=enrolled the SAME setup DOES rebind — proving the intent gate is