Files
felhom-agent/internal/storage/durableid.go
T
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

81 lines
2.6 KiB
Go

package storage
import (
"strings"
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
)
// deriveDurableID computes the DR-load-bearing durable identifier for a target (doc 03
// §7). It MUST be deterministic: the hub stores it and, on host loss, re-attaches the
// RIGHT physical target by it — the false-id failure mode is re-attaching the WRONG disk.
//
// Per type:
// - usb / local-dir: the filesystem UUID of the backing device (survives re-cabling /
// re-enumeration that renames /dev/sdX).
// - nfs / cifs: "server:export" (or "server:share") — the network identity.
// - pbs: "server:datastore" plus the cert fingerprint ("…#<fp>") — the repo
// identity (the fingerprint pins WHICH PBS, so a spoofed server is a different id).
// - lvmthin / lvm: "vgname/thinpool" (or "vgname") — informational but stable; the VG
// is local and not re-attached cross-host, so a stable name is enough.
// - local (builtin): the backing fs UUID if resolvable, else the path — informational.
//
// uuid is the already-resolved backing-device UUID ("" when unresolved); the caller
// resolves it once (it also needs it for nothing else, so we pass it in to avoid a second
// by-uuid scan).
func deriveDurableID(typ string, s proxmox.Storage, backingDevice, uuid string) string {
switch typ {
case hubTypeNFS:
if s.Server != "" && s.Export != "" {
return s.Server + ":" + s.Export
}
case hubTypeCIFS:
if s.Server != "" && s.Share != "" {
return s.Server + ":" + s.Share
}
case hubTypePBS:
repo := s.Datastore
if s.Server != "" {
repo = s.Server + ":" + s.Datastore
}
if repo != "" {
if s.Fingerprint != "" {
return repo + "#" + strings.ToLower(s.Fingerprint)
}
return repo
}
case hubTypeLVMThin, "lvm":
if s.VGName != "" {
if s.ThinPool != "" {
return s.VGName + "/" + s.ThinPool
}
return s.VGName
}
case hubTypeUSB, hubTypeLocalDir, hubTypeLocal:
if uuid != "" {
return "uuid:" + uuid
}
if backingDevice != "" {
return "dev:" + backingDevice
}
if s.Path != "" {
return "path:" + s.Path
}
}
// Fallback: a stable, unambiguous id from the storage name — never empty (an empty
// durable_id would defeat the hub's re-attach lookup).
return "store:" + s.Storage
}
// Reported storage-type strings (mirror hub's StorageType* constants without importing
// hub here for the bare strings — the observer maps to these).
const (
hubTypeLocalDir = "local-dir"
hubTypeLVMThin = "lvmthin"
hubTypeUSB = "usb"
hubTypeNFS = "nfs"
hubTypeCIFS = "cifs"
hubTypePBS = "pbs"
hubTypeLocal = "local"
)