Files
admin 27b68f043b v0.5.0-rc1: slice 5 Phase A — storage observe/report + watchdog (read-only, live)
Fill the slice-3 storage_targets stub and add the fast-poll storage watchdog.
Read-only this phase; the host-root surface (mounts/SMART/grow/destructive gate)
is Phase B. Hub-owned desired manifest is slice 10, so reconcile against it is
built-but-unfed.

- internal/storage: StorageTarget wire contract, durable_id derivation per type,
  HostReader seam (procfs/sysfs, root-free), Observer (storage_targets from
  ListStorage/NodeStorage + host reads, lvmthin thin-pool fill), and the watchdog
  (third daemon goroutine; debounced out-of-band report on a known target's
  attach/disconnect transition).
- proxmox.Storage: additive parse-only config fields (durable_id sources).
- collector StorageObserver seam; Loop.SetTrigger out-of-band report; daemon runs
  the watchdog as a third goroutine; StorageConfig knobs.
- cross-repo golden kept byte-identical with felhom.eu/hub; bidirectional key-set test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:59:05 +02:00

91 lines
2.7 KiB
Go

package storage
import (
"testing"
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
)
// durable_id is the DR-load-bearing field — the hub re-attaches the RIGHT physical target
// by it. Each type must derive deterministically; the false-id failure mode is
// re-attaching the WRONG disk, so this table pins the per-type shape.
func TestDeriveDurableID(t *testing.T) {
cases := []struct {
name string
typ string
s proxmox.Storage
backingDevice string
uuid string
want string
}{
{
name: "usb by fs-uuid",
typ: hubTypeUSB,
s: proxmox.Storage{Storage: "usb-backup", Path: "/mnt/usb-backup"},
uuid: "0fc6-abcd", want: "uuid:0fc6-abcd",
},
{
name: "local-dir by fs-uuid",
typ: hubTypeLocalDir, s: proxmox.Storage{Storage: "extra"}, uuid: "dead-beef",
want: "uuid:dead-beef",
},
{
name: "usb falls back to device when uuid unresolved",
typ: hubTypeUSB, s: proxmox.Storage{Storage: "usb-backup"}, backingDevice: "/dev/sdb1",
want: "dev:/dev/sdb1",
},
{
name: "nfs server:export",
typ: hubTypeNFS, s: proxmox.Storage{Storage: "nfs-arch", Server: "10.0.0.5", Export: "/export/b"},
want: "10.0.0.5:/export/b",
},
{
name: "cifs server:share",
typ: hubTypeCIFS, s: proxmox.Storage{Storage: "cifs", Server: "nas.local", Share: "backups"},
want: "nas.local:backups",
},
{
name: "pbs repo + fingerprint (lowercased)",
typ: hubTypePBS,
s: proxmox.Storage{Storage: "pbs", Server: "pbs.local", Datastore: "store1", Fingerprint: "AB:CD:EF"},
want: "pbs.local:store1#ab:cd:ef",
},
{
name: "pbs without fingerprint",
typ: hubTypePBS, s: proxmox.Storage{Storage: "pbs", Server: "pbs.local", Datastore: "store1"},
want: "pbs.local:store1",
},
{
name: "lvmthin vg/pool",
typ: hubTypeLVMThin, s: proxmox.Storage{Storage: "local-lvm", VGName: "pve", ThinPool: "data"},
want: "pve/data",
},
{
name: "lvm (thick) vg only",
typ: "lvm", s: proxmox.Storage{Storage: "vg0", VGName: "vg0"},
want: "vg0",
},
{
name: "local builtin by path when no uuid",
typ: hubTypeLocal, s: proxmox.Storage{Storage: "local", Path: "/var/lib/vz"},
want: "path:/var/lib/vz",
},
{
name: "unknown/unresolvable falls back to store name (never empty)",
typ: hubTypeNFS, s: proxmox.Storage{Storage: "broken-nfs"}, // missing server/export
want: "store:broken-nfs",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := deriveDurableID(c.typ, c.s, c.backingDevice, c.uuid)
if got != c.want {
t.Errorf("deriveDurableID = %q, want %q", got, c.want)
}
if got == "" {
t.Error("durable_id must never be empty")
}
})
}
}