v0.5.1: live-validation prep — fix unmounted-dir durable_id mis-id + watchdog UUID memory

Surfaced preparing the live USB validation on demo-felhom:
- observe.go: an unmounted removable dir-storage no longer falls through to the ROOT fs
  for its backing device/UUID — durable_id was becoming uuid:<root-uuid> (a DR mis-id that
  would re-attach the wrong disk). Now derived only from the target's own mountpoint;
  unmounted → no device + stable store:<name> durable_id. Removed containingMountDevice.
- watchdog.go: remember the fs-UUID observed while attached and backfill it onto the
  re-mount target, so re-mount works even if the known-set cache refreshed mid-drop
  (doc 03 §7 "sourced from the existing definition").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 11:01:40 +02:00
parent 9d6e49236c
commit 77b4f21450
6 changed files with 90 additions and 23 deletions
+8 -22
View File
@@ -161,14 +161,19 @@ func (o *Observer) snapshot(ctx context.Context) ([]observed, error) {
func (o *Observer) build(s proxmox.Storage, mounts []Mount) observed {
category := categorize(s.Type)
// Resolve the backing device + mount path for dir-like targets.
// Resolve the backing device + mount path for dir-like targets — ONLY from the
// target's OWN mountpoint. We deliberately do NOT fall through to the containing
// filesystem (e.g. root): an unmounted removable dir-storage's mountpoint reverts to a
// bare directory on root, and resolving its UUID/durable_id to ROOT's UUID would be a
// catastrophic DR mis-id (the hub would re-attach the wrong disk). When the target is
// not its own mount, we leave the device/UUID unknown and the durable_id falls back to a
// stable store id (never another fs's UUID). The authoritative UUID for re-attach comes
// from a prior attached observation (watchdog memory) or the hub manifest (slice 10).
var backingDevice, mountPath string
var exactMount bool
if category == catDir {
if dev, mp, ok := exactMountDevice(mounts, s.Path); ok {
backingDevice, mountPath, exactMount = dev, mp, true
} else if dev, ok := containingMountDevice(mounts, s.Path); ok {
backingDevice = dev // for the class hint only; not its own mount
}
}
@@ -396,25 +401,6 @@ func exactMountDevice(mounts []Mount, path string) (device, mountPoint string, o
return "", "", false
}
// containingMountDevice finds the device of the longest mountpoint that is a prefix of
// path (the filesystem that path lives on) — used only for the class-hint disk lookup.
func containingMountDevice(mounts []Mount, path string) (device string, ok bool) {
if path == "" {
return "", false
}
clean := cleanMountPath(path)
best := -1
for _, m := range mounts {
mp := cleanMountPath(m.MountPoint)
if clean == mp || strings.HasPrefix(clean, mp+"/") || mp == "/" {
if len(mp) > best {
best, device, ok = len(mp), m.Device, true
}
}
}
return device, ok
}
func cleanMountPath(p string) string {
p = strings.TrimRight(p, "/")
if p == "" {