912f7c127e
gates / gates (push) Successful in 27s
- internal place: the record /var/lib/felhom-agent/internal-place.json -> mounted at /mnt/belso by fs-UUID like an enrolled drive (startup + 20 s tick), kind "internal" in GET /disks; never takes a household drive's /mnt/belso; eject/decommission/format refused (dm on the system disk). - configs/felhom-internal-place: the ruled size rule and the only lvcreate/mkfs of a system-disk volume (installer + the new signed op add_internal_place, operator key, no parameters). - partitions listed per free disk (empty / filesystem / in_use); a data-bearing part's wipe id binds disk id + part number + fs-UUID. - felhom-mkfs-guarded: Felhom's own sibling drive no longer blocks formatting the free part; an unlistable disk refuses. Tests red-proved (script mutations, Go mutations, the old wrapper measured against the new tests). Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
250 lines
9.8 KiB
Go
250 lines
9.8 KiB
Go
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)
|
|
}
|
|
}
|