package localapi import ( "context" "errors" "io" "log/slog" "net/http" "os/exec" "path/filepath" "runtime" "sync" "testing" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" "gitea.dooplex.hu/admin/felhom-agent/internal/storage" ) // The internal data place (felhom.eu architecture/16 §5, agent v0.158.0): mounted like an enrolled drive, but never // at the cost of a household drive, never formatted, ejected or decommissioned, and the same disk as sys_drive. const ipUUID = "0b6e4c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c" const ipDev = "/dev/mapper/pve-felhom--belso" // ipHost is a HostReader whose mount table the test changes, and whose by-uuid map it controls. type ipHost struct { mu sync.Mutex mounts []storage.Mount uuids map[string]string // device → fs-UUID } func (h *ipHost) Mounts() ([]storage.Mount, error) { h.mu.Lock() defer h.mu.Unlock() return append([]storage.Mount(nil), h.mounts...), nil } func (h *ipHost) ResolveUUID(dev string) (string, bool) { u, ok := h.uuids[dev]; return u, ok } func (h *ipHost) DeviceExists(string) bool { return true } func (h *ipHost) Rotational(string) (bool, bool) { return false, false } func (h *ipHost) Removable(string) (bool, bool) { return false, false } func (h *ipHost) BlockSlaves(string) ([]string, bool) { return nil, false } func newIPHost(extra ...storage.Mount) *ipHost { return &ipHost{ // root on a whole-disk partition (as sysOnSDA): the system disk resolves, so a USB drive is user-data. mounts: append([]storage.Mount{{Device: "/dev/sda1", MountPoint: "/", FSType: "ext4"}}, extra...), uuids: map[string]string{ipDev: ipUUID, "/dev/sdb1": "99999999-aaaa-bbbb-cccc-dddddddddddd"}, } } type ipRig struct { srv *Server disks *fakeDiskOps intent *fakeIntent } func newIPRig(t *testing.T, host *ipHost, known []storage.KnownTarget, record bool) ipRig { t.Helper() dir := t.TempDir() file := filepath.Join(dir, InternalPlaceFile) if record { if err := WriteInternalPlace(file, InternalPlaceRecord{FSUUID: ipUUID, GiB: 183, MadeBy: "installer"}); err != nil { t.Fatal(err) } } d, in := &fakeDiskOps{}, newFakeIntent() srv, err := NewServer(Options{ ListenAddr: "127.0.0.1:0", Guests: &fakeGuests{}, Backups: &fakeBackups{}, Store: &fakeStore{}, Storage: fakeStorage{}, Tokens: staticTokens{"A": 8200}, Disks: d, DiskGate: &fakeGate{}, HostReader: host, Intent: in, DriveTargets: fakeKnownTargets{drives: known}, InternalPlaceFile: file, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatal(err) } srv.baseCtx = context.Background() srv.resolveStorageDevice = func(id string) (string, error) { if id == "uuid:"+ipUUID { return "/dev/dm-7", nil } return "", errors.New("absent") } return ipRig{srv, d, in} } func TestInternalPlace_NoRecord_DoesNothing(t *testing.T) { r := newIPRig(t, newIPHost(), nil, false) r.srv.EnsureInternalPlace(context.Background()) if len(r.disks.mountCalls) != 0 { t.Fatalf("no record: no mount may be made, got %+v", r.disks.mountCalls) } } func TestInternalPlace_Record_MountsByUUIDAtMntBelso(t *testing.T) { r := newIPRig(t, newIPHost(), nil, true) r.srv.EnsureInternalPlace(context.Background()) if len(r.disks.mountCalls) != 1 { t.Fatalf("want exactly one EnsureMount, got %+v", r.disks.mountCalls) } got := r.disks.mountCalls[0] if got.UUID != ipUUID || got.Where != InternalPlaceWhere || got.FSType != "ext4" || got.Name != InternalPlaceName { t.Fatalf("EnsureMount spec: %+v", got) } } func TestInternalPlace_AlreadyMounted_EnrolledOnceNoRemount(t *testing.T) { host := newIPHost(storage.Mount{Device: ipDev, MountPoint: InternalPlaceWhere, FSType: "ext4"}) r := newIPRig(t, host, nil, true) r.srv.EnsureInternalPlace(context.Background()) r.srv.EnsureInternalPlace(context.Background()) if len(r.disks.mountCalls) != 0 { t.Fatalf("already mounted: no EnsureMount, got %+v", r.disks.mountCalls) } if r.intent.Get("uuid:"+ipUUID) != storage.IntentEnrolled { t.Fatalf("the internal place must be recorded enrolled") } } // A household drive called „belso" keeps its mount point: the internal place never takes it. // Positive control: TestInternalPlace_Record_MountsByUUIDAtMntBelso (the same record, /mnt/belso free). func TestInternalPlace_NeverTakesAHouseholdDriveNamedBelso(t *testing.T) { mounted := newIPHost(storage.Mount{Device: "/dev/sdb1", MountPoint: InternalPlaceWhere, FSType: "ext4"}) r := newIPRig(t, mounted, nil, true) r.srv.EnsureInternalPlace(context.Background()) if len(r.disks.mountCalls) != 0 || r.intent.Get("uuid:"+ipUUID) != "" { t.Fatalf("a foreign filesystem at /mnt/belso: nothing may be mounted or recorded, got %+v", r.disks.mountCalls) } unplugged := []storage.KnownTarget{{Name: "belso", UUID: "99999999-aaaa-bbbb-cccc-dddddddddddd", MountPath: InternalPlaceWhere, MountBacked: true}} r2 := newIPRig(t, newIPHost(), unplugged, true) r2.srv.EnsureInternalPlace(context.Background()) if len(r2.disks.mountCalls) != 0 { t.Fatalf("an unplugged household drive's unit for /mnt/belso must not be overwritten, got %+v", r2.disks.mountCalls) } } func TestInternalPlace_VolumeAbsent_NoMount(t *testing.T) { r := newIPRig(t, newIPHost(), nil, true) r.srv.resolveStorageDevice = func(string) (string, error) { return "", errors.New("absent") } r.srv.EnsureInternalPlace(context.Background()) if len(r.disks.mountCalls) != 0 { t.Fatalf("absent volume: no mount, got %+v", r.disks.mountCalls) } } // The household cannot eject or decommission it: its device is a device-mapper volume on the system disk. // Positive control: the same calls for a whole-disk USB drive (/dev/sdb1) are not refused by role. func TestInternalPlace_EjectAndDecommissionRefused(t *testing.T) { host := newIPHost( storage.Mount{Device: ipDev, MountPoint: InternalPlaceWhere, FSType: "ext4"}, storage.Mount{Device: "/dev/sdb1", MountPoint: "/mnt/hdd_1", FSType: "ext4"}, ) r := newIPRig(t, host, nil, true) h := r.srv.Handler() for _, route := range []string{"/disks/eject", "/disks/decommission"} { if w := do(t, h, "POST", route, "A", `{"where":"/mnt/belso"}`); w.Code != http.StatusForbidden { t.Fatalf("%s /mnt/belso: want 403, got %d %s", route, w.Code, w.Body.String()) } } if w := do(t, h, "POST", "/disks/eject", "A", `{"where":"/mnt/hdd_1"}`); w.Code == http.StatusForbidden { t.Fatalf("positive control: a USB drive's eject must not be refused by role, got %d %s", w.Code, w.Body.String()) } for _, u := range r.disks.unmountCalls { if u == InternalPlaceWhere { t.Fatalf("the internal place must never be unmounted, got %+v", r.disks.unmountCalls) } } } // The consequence `16` §5 needs: a copy between the internal place and sys_drive is the SAME disk (build A then never // counts it), and the place is reported as kind internal. type ipIdentHost struct{ fakeIdentHost } func (h *ipIdentHost) KernelName(dev string) (string, bool) { if dev == ipDev { return "dm-7", true } return h.fakeIdentHost.KernelName(dev) } func (h *ipIdentHost) Slaves(n string) []string { if n == "dm-7" { return []string{"dm-3"} // felhom-belso → the thin pool } if n == "dm-3" { return []string{"sda3"} // the pool → the system disk's LVM partition } return nil } func TestInternalPlace_IsTheSystemDiskAndKindInternal(t *testing.T) { ih := &ipIdentHost{fakeIdentHost{ virt: "none", parent: map[string]string{"sda3": "sda"}, props: map[string]map[string]string{"sda": {"ID_BUS": "ata", "ID_MODEL": "SanDisk", "ID_SERIAL_SHORT": "1"}}, mounts: []storage.Mount{ {Device: "/dev/mapper/pve-root", MountPoint: "/", FSType: "ext4"}, {Device: "/dev/mapper/pve-root", MountPoint: "/mnt/felhom-drives", FSType: "ext4"}, {Device: ipDev, MountPoint: InternalPlaceWhere, FSType: "ext4"}, {Device: ipDev, MountPoint: "/mnt/felhom-drives/belso", FSType: "ext4"}, }, }} fg := &fakeGuestsCfg{mounts: map[int]map[string]string{8200: { "mp0": "local-lvm:vm-8200-disk-1,mp=/var/lib/felhom,backup=1,size=100G", "mp8": "/mnt/felhom-drives,mp=/mnt/felhom-drives", }}} dir := t.TempDir() file := filepath.Join(dir, InternalPlaceFile) if err := WriteInternalPlace(file, InternalPlaceRecord{FSUUID: ipUUID}); err != nil { t.Fatal(err) } srv, err := NewServer(Options{ ListenAddr: "127.0.0.1:0", Guests: fg, Backups: &fakeBackups{}, Store: &fakeStore{}, Storage: fakeStorage{targets: []hub.StorageTarget{provenSSD}}, Tokens: staticTokens{"A": 8200}, HostReader: fakeHostReader{mounts: ih.mounts}, Identity: &storage.IdentityResolver{Host: ih}, InternalPlaceFile: file, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatal(err) } srv.baseCtx = context.Background() got, code := sameDisk(t, srv.Handler(), "/mnt/felhom-drives/belso/felhom-data/jellyfin", "/mnt/sys_drive") if code != http.StatusOK || got.Different || !got.Proven || got.Reason != "same_disk" { t.Fatalf("internal place vs sys_drive: want same disk, proven; got %d %+v", code, got) } if got.A.Kind != storage.PlaceKindInternal { t.Fatalf("kind: want %q, got %q", storage.PlaceKindInternal, got.A.Kind) } } func TestInternalPlaceRecord_RefusesANonUUID(t *testing.T) { file := filepath.Join(t.TempDir(), InternalPlaceFile) if err := WriteInternalPlace(file, InternalPlaceRecord{FSUUID: "../../etc"}); err == nil { t.Fatal("a non-UUID must not be written") } } // The root helper's own tests (configs/test_felhom_internal_place.py — the ruled size table, never formatting what // holds anything) run with the Go suite. func TestInternalPlaceHelperPythonSuite(t *testing.T) { py, err := exec.LookPath("python3") if err != nil { t.Skip("python3 not found") } _, file, _, _ := runtime.Caller(0) script := filepath.Join(filepath.Dir(file), "..", "..", "configs", "test_felhom_internal_place.py") out, err := exec.Command(py, script).CombinedOutput() if err != nil { t.Fatalf("helper suite failed: %v\n%s", err, out) } }