v0.95.0: SMART coverage — union-path drives + LVM/dm root + device model
Implements SPIKE-smart-coverage-2026-07-25 fixes B+A (additive; MinAgent unchanged). Fix B: storage.SmartReader.SMARTForBacking wired into the /disks union path (localapi Smart seam) so registry/USB drives get a real SMART read (watchdog Known stays enrich-free). Fix A: smartDeviceFor resolves dm/LVM to the whole disk via /sys/block/<dm>/slaves (recursive; skips >1-disk); the builtin local dir on the LVM root gets a SMART-only device from its containing filesystem (never touches backing/durable_id). SmartSummary.ModelName captured from smartctl. Fix C (-d sat) stays rejected. Tests + red-proofs (dm multi-disk skip, enrich smartHint, union routing); Known-path-never-SMARTs asserted.
This commit is contained in:
+41
-10
@@ -58,6 +58,9 @@ type observed struct {
|
||||
known KnownTarget
|
||||
src proxmox.Storage
|
||||
cat storageCategory
|
||||
// smartHint (v0.95.0) is a SMART-ONLY whole-disk device for a dir-storage whose own backing is
|
||||
// empty (the builtin `local` on the shared LVM root). Never assigned to BackingDevice/durable_id.
|
||||
smartHint string
|
||||
}
|
||||
|
||||
// Observe builds the reported []hub.StorageTarget. A non-nil error means the Proxmox read
|
||||
@@ -84,13 +87,22 @@ func (o *Observer) enrich(ctx context.Context, ob observed) hub.StorageTarget {
|
||||
if o.ops == nil {
|
||||
return t
|
||||
}
|
||||
// SMART: only for dir-backed targets with a resolvable whole-disk device.
|
||||
if ob.cat == catDir && t.BackingDevice != "" {
|
||||
if dev, ok := smartDeviceFor(t.BackingDevice); ok {
|
||||
if sm, err := o.ops.SMART(ctx, dev); err != nil {
|
||||
o.logger.Warn("storage: SMART read failed; health UNKNOWN", "device", dev, "err", err)
|
||||
} else {
|
||||
t.Smart = sm
|
||||
// SMART: for dir-backed targets. The device is the target's own backing (a USB/local-dir exact
|
||||
// mount) OR, for a dir on a shared filesystem whose backing is deliberately empty (the builtin
|
||||
// `local` on the LVM root — removable-safety guard in build), the SMART-only hint build() resolved
|
||||
// from the containing filesystem. smartDeviceFor then resolves dm/LVM/partition to the whole disk.
|
||||
if ob.cat == catDir {
|
||||
smartDev := t.BackingDevice
|
||||
if smartDev == "" {
|
||||
smartDev = ob.smartHint
|
||||
}
|
||||
if smartDev != "" {
|
||||
if dev, ok := smartDeviceFor(smartDev); ok {
|
||||
if sm, err := o.ops.SMART(ctx, dev); err != nil {
|
||||
o.logger.Warn("storage: SMART read failed; health UNKNOWN", "device", dev, "err", err)
|
||||
} else {
|
||||
t.Smart = sm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,10 +250,23 @@ func (o *Observer) build(s proxmox.Storage, mounts []Mount) observed {
|
||||
}
|
||||
}
|
||||
|
||||
// SMART-only device hint (v0.95.0): a dir-storage that lives INSIDE a shared filesystem (the
|
||||
// builtin `local` on the LVM root) has empty backing by design, yet we can still read the PHYSICAL
|
||||
// disk's SMART by resolving its containing filesystem. Gated to catDir + no own backing + reachable,
|
||||
// so an UNPLUGGED removable (disconnected) never reads root's SMART, and a mounted removable uses
|
||||
// its own backing instead.
|
||||
smartHint := ""
|
||||
if category == catDir && backingDevice == "" && reachable {
|
||||
if dev, ok := containingMountDevice(mounts, s.Path); ok {
|
||||
smartHint = dev
|
||||
}
|
||||
}
|
||||
|
||||
return observed{
|
||||
target: tgt,
|
||||
src: s,
|
||||
cat: category,
|
||||
target: tgt,
|
||||
src: s,
|
||||
cat: category,
|
||||
smartHint: smartHint,
|
||||
known: KnownTarget{
|
||||
Name: s.Storage,
|
||||
Type: typ,
|
||||
@@ -260,6 +285,12 @@ func (o *Observer) build(s proxmox.Storage, mounts []Mount) observed {
|
||||
// smartctl (which targets the disk, not the partition). Returns ok=false when the result
|
||||
// isn't a recognized raw disk (e.g. device-mapper / LVM), so SMART is simply skipped.
|
||||
func smartDeviceFor(device string) (string, bool) {
|
||||
// Device-mapper / LVM (Fix A, v0.95.0): resolve to the single backing whole disk via sysfs
|
||||
// slaves. This is what finally covers the system SSD under `pve-root`. The sysfs resolution IS
|
||||
// the existence check, so we do NOT re-run ValidateSMARTDevice on its result.
|
||||
if strings.HasPrefix(device, "/dev/dm-") || strings.HasPrefix(device, "/dev/mapper/") {
|
||||
return dmWholeDisk(device)
|
||||
}
|
||||
dev := device
|
||||
if m := reNVMePart.FindStringSubmatch(device); m != nil {
|
||||
dev = m[1] // /dev/nvme0n1p2 -> /dev/nvme0n1
|
||||
|
||||
Reference in New Issue
Block a user