3a9be73875
Replaces the per-drive 'pct set -mpN' bind with ONE permanent parent bind /mnt/felhom-drives plus host-side felhom-data swaps underneath it (propagates into the running guest live, no pct, no reboot; C1-immune; confined; fail-closed when absent). EnsureSharedParent installs a boot unit ordered Before=pve-guests. ReassertGuestBinds is now a pure host-side reconcile. /disks reports GuestPath + BoundUnderParent for the controller repoint+gate. Non-hollow tests + companions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
3.7 KiB
Go
112 lines
3.7 KiB
Go
package localapi
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
|
)
|
|
|
|
func tempBindStore(t *testing.T) *GuestBindStore {
|
|
t.Helper()
|
|
gb, err := OpenGuestBindStore(filepath.Join(t.TempDir(), "guest-binds.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return gb
|
|
}
|
|
|
|
func reassertServer(t *testing.T, ga GuestAttacher, sv StorageView, mounts map[int]map[string]string, gb *GuestBindStore) *Server {
|
|
t.Helper()
|
|
srv, err := NewServer(Options{
|
|
ListenAddr: "127.0.0.1:0",
|
|
Guests: &fakeGuestsCfg{mounts: mounts},
|
|
Backups: &fakeBackups{}, Store: &fakeStore{},
|
|
Storage: sv, Tokens: staticTokens{"A": 8200},
|
|
GuestAttach: ga, GuestBinds: gb,
|
|
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv.baseCtx = context.Background()
|
|
return srv
|
|
}
|
|
|
|
// usbPresent is a storage view with felhom-usb present at /mnt/felhom-usb (durable uuid:usb-1).
|
|
func usbPresent() fakeStorage {
|
|
return fakeStorage{targets: []hub.StorageTarget{
|
|
{Name: "usb", Type: hub.StorageTypeUSB, BackingDevice: "/dev/sdb1", MountPath: "/mnt/felhom-usb", DurableID: "uuid:usb-1"},
|
|
}}
|
|
}
|
|
|
|
// TestReassertGuestBinds_RestoresMissingBind is the F9/reconnect core proof: an enrolled, host-present
|
|
// drive is auto-bound under the shared parent on the startup reconcile — host-side (AttachDrive), with NO
|
|
// pct and NO manual guest-attach call.
|
|
//
|
|
// COMPANION GUARD: the legacy `pct set -mpN` AttachBind must NOT be used — a pre-intermediary impl that
|
|
// still re-added a config mp would trip ga.count()!=0.
|
|
func TestReassertGuestBinds_RestoresMissingBind(t *testing.T) {
|
|
gb := tempBindStore(t)
|
|
if err := gb.Record(8200, "uuid:usb-1"); err != nil { // enrolled at a prior boot
|
|
t.Fatal(err)
|
|
}
|
|
ga := &fakeGuestAttacher{}
|
|
srv := reassertServer(t, ga, usbPresent(), nil, gb)
|
|
|
|
srv.ReassertGuestBinds(context.Background())
|
|
|
|
if ga.attachDriveCount() != 1 || ga.attachDrives[0] != "/mnt/felhom-usb" {
|
|
t.Fatalf("AttachDrive = %v, want one call for /mnt/felhom-usb (host-side reconcile)", ga.attachDrives)
|
|
}
|
|
if ga.count() != 0 {
|
|
t.Fatalf("legacy pct AttachBind called (%d) — reconcile must be host-side only", ga.count())
|
|
}
|
|
if ga.ensureParentN < 1 {
|
|
t.Fatalf("EnsureSharedParent must run before binding drives under the parent")
|
|
}
|
|
}
|
|
|
|
// TestReassertGuestBinds_SkipsAbsentDurable: an enrolled drive whose durable-id is NOT currently present
|
|
// (unplugged / swapped for a different disk) must NOT be auto-bound — the "on durable-id match" safety.
|
|
func TestReassertGuestBinds_SkipsAbsentDurable(t *testing.T) {
|
|
gb := tempBindStore(t)
|
|
_ = gb.Record(8200, "uuid:usb-1")
|
|
ga := &fakeGuestAttacher{}
|
|
srv := reassertServer(t, ga, fakeStorage{}, nil, gb) // empty storage view → absent
|
|
|
|
srv.ReassertGuestBinds(context.Background())
|
|
if ga.attachDriveCount() != 0 {
|
|
t.Fatalf("AttachDrive called %d times — must NOT auto-bind an absent/swapped drive", ga.attachDriveCount())
|
|
}
|
|
}
|
|
|
|
// TestGuestBindStore_Persist round-trips the store across reopen (the record must survive an agent
|
|
// restart, since that is exactly when the re-assert runs).
|
|
func TestGuestBindStore_Persist(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "guest-binds.json")
|
|
gb, err := OpenGuestBindStore(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = gb.Record(8200, "uuid:usb-1")
|
|
_ = gb.Record(8200, "uuid:usb-1") // idempotent
|
|
_ = gb.Record(9300, "byid:wwn-x")
|
|
|
|
re, err := OpenGuestBindStore(path) // simulate restart
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g := re.Guests()
|
|
if len(g[8200]) != 1 || g[8200][0] != "uuid:usb-1" {
|
|
t.Fatalf("vmid 8200 = %v, want [uuid:usb-1]", g[8200])
|
|
}
|
|
if len(g[9300]) != 1 || g[9300][0] != "byid:wwn-x" {
|
|
t.Fatalf("vmid 9300 = %v, want [byid:wwn-x]", g[9300])
|
|
}
|
|
}
|