controllerswap: F1 verify hardening — reject crash-looping no-healthcheck image v0.47.0

controllerHealthy reads RestartCount (running&&rc>0 -> not ok) + signals needsDwell for no-healthcheck;
verify requires verifyDwell(=3) consecutive ok polls for a no-healthcheck image (real healthcheck
trusted immediately). Closes the F1 hole (alpine crash-loop passed the point-in-time check). Red-proof
+ dwell + real-image tests. No sudoers/orchestration change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
This commit is contained in:
2026-06-29 22:45:41 +02:00
parent bb548e3c5a
commit 3844df7c23
8 changed files with 218 additions and 57 deletions
+21 -11
View File
@@ -51,7 +51,11 @@ func (f *fakeDiskOps) Unmount(_ context.Context, where string) error {
f.mu.Unlock()
return nil
}
func (f *fakeDiskOps) formatted() []string { f.mu.Lock(); defer f.mu.Unlock(); return append([]string(nil), f.formatCalls...) }
func (f *fakeDiskOps) formatted() []string {
f.mu.Lock()
defer f.mu.Unlock()
return append([]string(nil), f.formatCalls...)
}
type fakeGate struct {
mu sync.Mutex
@@ -80,11 +84,11 @@ func (f fakeGuestList) ListLXC(context.Context) ([]proxmox.Guest, error) { retur
// a different whole-disk (e.g. /dev/sdb1) classifies as user-data without touching the real host.
type fakeHostReader struct{ mounts []storage.Mount }
func (f fakeHostReader) Mounts() ([]storage.Mount, error) { return f.mounts, nil }
func (f fakeHostReader) ResolveUUID(string) (string, bool) { return "", false }
func (f fakeHostReader) DeviceExists(string) bool { return true }
func (f fakeHostReader) Rotational(string) (bool, bool) { return false, false }
func (f fakeHostReader) Removable(string) (bool, bool) { return false, false }
func (f fakeHostReader) Mounts() ([]storage.Mount, error) { return f.mounts, nil }
func (f fakeHostReader) ResolveUUID(string) (string, bool) { return "", false }
func (f fakeHostReader) DeviceExists(string) bool { return true }
func (f fakeHostReader) Rotational(string) (bool, bool) { return false, false }
func (f fakeHostReader) Removable(string) (bool, bool) { return false, false }
// sysOnSDA is the default system-disk fixture (root on /dev/sda) used by the disk-server test helpers.
func sysOnSDA() fakeHostReader {
@@ -345,7 +349,7 @@ func TestEject_RoleGated(t *testing.T) {
sv := fakeStorage{targets: []hub.StorageTarget{
{Name: "bulk", Type: hub.StorageTypeUSB, BackingDevice: "/dev/sdb1", MountPath: "/mnt/bulk"}, // user-data
{Name: "local", Type: "local", MountPath: "/var/lib/vz"}, // system (builtin dir)
{Name: "felhom-pbs", Type: hub.StorageTypePBS, MountPath: "/mnt/pbs"}, // backup (PBS)
{Name: "felhom-pbs", Type: hub.StorageTypePBS, MountPath: "/mnt/pbs"}, // backup (PBS)
}}
// system mount → refused, no Unmount.
@@ -438,7 +442,7 @@ func (f *fakeGuestAttacher) AttachDrive(_ context.Context, _ int, where string)
return StablePathForRaw(where), nil
}
func (f *fakeGuestAttacher) GuestSeesMount(_ context.Context, _ int, _ string) bool { return true }
func (f *fakeGuestAttacher) GuestBootID(_ context.Context, _ int) string { return "boot-1" }
func (f *fakeGuestAttacher) GuestBootID(_ context.Context, _ int) string { return "boot-1" }
func (f *fakeGuestAttacher) attachDriveCount() int {
f.mu.Lock()
defer f.mu.Unlock()
@@ -480,7 +484,11 @@ func (f *fakeGuestAttacher) RebootGuest(_ context.Context, vmid int) error {
f.reboots = append(f.reboots, vmid)
return nil
}
func (f *fakeGuestAttacher) rebootCount() int { f.mu.Lock(); defer f.mu.Unlock(); return len(f.reboots) }
func (f *fakeGuestAttacher) rebootCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return len(f.reboots)
}
func newAttachServer(t *testing.T, ga GuestAttacher, mounts map[int]map[string]string) http.Handler {
t.Helper()
@@ -646,8 +654,10 @@ func (f *fakeGuestsCfg) GuestConfig(_ context.Context, vmid int) (proxmox.GuestC
}
return proxmox.GuestConfig{Extra: extra}, nil
}
func (f *fakeGuestsCfg) Snapshot(context.Context, int, string, string) (string, error) { return "", nil }
func (f *fakeGuestsCfg) Rollback(context.Context, int, string) (string, error) { return "", nil }
func (f *fakeGuestsCfg) Snapshot(context.Context, int, string, string) (string, error) {
return "", nil
}
func (f *fakeGuestsCfg) Rollback(context.Context, int, string) (string, error) { return "", nil }
func (f *fakeGuestsCfg) WaitTask(context.Context, string, proxmox.WaitOptions) (proxmox.TaskStatus, error) {
return proxmox.TaskStatus{ExitStatus: "OK"}, nil
}