package localapi import ( "context" "encoding/json" "io" "log/slog" "net/http" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" "gitea.dooplex.hu/admin/felhom-agent/internal/storage" ) // POST /disks/same-disk (`16` §4) — the controller's Tier-2 question, answered from the agent's own read. type fakeIdentHost struct { mounts []storage.Mount parent map[string]string props map[string]map[string]string virt string } func (f *fakeIdentHost) Mounts() ([]storage.Mount, error) { return f.mounts, nil } func (f *fakeIdentHost) KernelName(dev string) (string, bool) { if len(dev) > 5 && dev[:5] == "/dev/" { return dev[5:], true } return "", false } func (f *fakeIdentHost) Parent(n string) (string, bool) { p, ok := f.parent[n]; return p, ok } func (f *fakeIdentHost) Slaves(string) []string { return nil } func (f *fakeIdentHost) LoopBackingFile(string) (string, bool) { return "", false } func (f *fakeIdentHost) UdevProps(n string) map[string]string { return f.props[n] } func (f *fakeIdentHost) Virt() string { return f.virt } // sameDiskRig: guest 8200 (token A) with mp0 on local-lvm (the system SSD, proven) + the given mounts. func sameDiskRig(t *testing.T, host *fakeIdentHost, localLVM hub.StorageTarget) http.Handler { t.Helper() fg := &fakeGuestsCfg{mounts: map[int]map[string]string{8200: { "mp0": "local-lvm:vm-8200-disk-1,mp=/var/lib/felhom,backup=1,size=70G", "mp8": "/mnt/felhom-drives,mp=/mnt/felhom-drives", }}} srv, err := NewServer(Options{ ListenAddr: "127.0.0.1:0", Guests: fg, Backups: &fakeBackups{}, Store: &fakeStore{}, Storage: fakeStorage{targets: []hub.StorageTarget{localLVM}}, Tokens: staticTokens{"A": 8200}, HostReader: fakeHostReader{mounts: host.mounts}, Identity: &storage.IdentityResolver{Host: host}, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatal(err) } srv.baseCtx = context.Background() return srv.Handler() } var provenSSD = hub.StorageTarget{Name: "local-lvm", Type: hub.StorageTypeLVMThin, Kind: "system", PhysicalDisks: []string{"serial:ata:SanDisk:1"}, Identity: "proven"} func baseHost() *fakeIdentHost { return &fakeIdentHost{ virt: "none", parent: map[string]string{"sdc1": "sdc", "sdc2": "sdc"}, props: map[string]map[string]string{ "sdb": {"ID_BUS": "ata", "ID_MODEL": "TOSHIBA", "ID_SERIAL_SHORT": "65NOP3HDT"}, "sdc": {"ID_BUS": "usb", "ID_MODEL": "Portable", "ID_SERIAL_SHORT": "NAAB7C21"}, }, mounts: []storage.Mount{ {Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}, {Device: "/dev/mapper/pve-root", MountPoint: "/mnt/felhom-drives", FSType: "ext4"}, {Device: "/dev/sdb", MountPoint: "/mnt/felhom-drives/hdd_1", FSType: "ext4"}, {Device: "/dev/sdc1", MountPoint: "/mnt/felhom-drives/a", FSType: "ext4"}, {Device: "/dev/sdc2", MountPoint: "/mnt/felhom-drives/b", FSType: "ext4"}, {Device: "//nas/films", MountPoint: "/mnt/felhom-drives/nas", FSType: "cifs"}, }, } } func sameDisk(t *testing.T, h http.Handler, a, b string) (SameDiskResponse, int) { t.Helper() body, _ := json.Marshal(map[string]string{"a": a, "b": b}) w := do(t, h, "POST", "/disks/same-disk", "A", string(body)) var env struct { Data SameDiskResponse `json:"data"` } _ = json.Unmarshal(w.Body.Bytes(), &env) return env.Data, w.Code } func TestSameDisk_DriveVsSystemData_DifferentProven(t *testing.T) { h := sameDiskRig(t, baseHost(), provenSSD) got, code := sameDisk(t, h, "/mnt/felhom-drives/hdd_1/felhom-data/paperless", "/mnt/sys_drive") if code != http.StatusOK || !got.Different || !got.Proven || got.Reason != "" { t.Fatalf("drive vs sys_drive: want different+proven, got %d %+v", code, got) } if got.B.Kind != "system" || got.A.Kind != "disk" { t.Fatalf("kinds: got a=%q b=%q", got.A.Kind, got.B.Kind) } } func TestSameDisk_TwoPartitionsOfOneDisk_SameProven(t *testing.T) { h := sameDiskRig(t, baseHost(), provenSSD) got, _ := sameDisk(t, h, "/mnt/felhom-drives/a", "/mnt/felhom-drives/b/x") if got.Different || !got.Proven || got.Reason != "same_disk" || got.A.Kind != "partition" { t.Fatalf("two partitions: want same+proven, got %+v", got) } } func TestSameDisk_InsideAVM_NotProven(t *testing.T) { host := baseHost() host.virt = "kvm" h := sameDiskRig(t, host, hub.StorageTarget{Name: "local-lvm", Identity: "unknown", IdentityReason: "vm", PhysicalDisks: []string{"serial:scsi:QEMU_HARDDISK:drive-scsi0"}}) got, _ := sameDisk(t, h, "/mnt/felhom-drives/hdd_1", "/mnt/sys_drive") if got.Proven || got.Reason != "vm" { t.Fatalf("inside a VM: want not proven (vm), got %+v", got) } } func TestSameDisk_NetworkAndUnmapped(t *testing.T) { h := sameDiskRig(t, baseHost(), provenSSD) if got, _ := sameDisk(t, h, "/mnt/felhom-drives/nas", "/mnt/sys_drive"); got.Proven || got.Reason != "network" { t.Fatalf("a NAS: want not proven (network), got %+v", got) } // An ABSENT drive: its directory sits on the root disk — must be unmapped, never "the system disk". if got, _ := sameDisk(t, h, "/mnt/felhom-drives/gone", "/mnt/sys_drive"); got.Proven || got.Reason != "unmapped" || got.A.Mapped { t.Fatalf("an absent drive: want unmapped, got %+v", got) } if got, _ := sameDisk(t, h, "/opt/elsewhere", "/mnt/sys_drive"); got.Proven || got.Reason != "unmapped" { t.Fatalf("an unknown guest path: want unmapped, got %+v", got) } } func TestSameDisk_ScopedAndValidated(t *testing.T) { h := sameDiskRig(t, baseHost(), provenSSD) if w := do(t, h, "POST", "/disks/same-disk", "A", `{"vmid":9300,"a":"/mnt/sys_drive","b":"/mnt/sys_drive"}`); w.Code != http.StatusForbidden { t.Fatalf("another guest's vmid: got %d want 403", w.Code) } if w := do(t, h, "POST", "/disks/same-disk", "A", `{"a":"relative","b":"/mnt/sys_drive"}`); w.Code != http.StatusBadRequest { t.Fatalf("a relative path: got %d want 400", w.Code) } if w := do(t, h, "POST", "/disks/same-disk", "", `{"a":"/x","b":"/y"}`); w.Code != http.StatusUnauthorized { t.Fatalf("no token: got %d want 401", w.Code) } }