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>
81 lines
2.6 KiB
Go
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"
|
|
)
|