27b68f043b
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>
91 lines
2.7 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|
|
}
|