mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Stop a replaced PVE disk inheriting the old disk's evidence (#2648)
## Problem The Proxmox disk poller keeps evidence it could not collect this poll by looking up the previous poll's record of the disk: by serial, then WWN, then the Proxmox source ID (`instance-node-devpath`), then the device token. The source ID and device token name a slot, not a disk, and a replacement in the same slot keeps both. So: - a replacement reporting a different serial or WWN took the old disk's retained temperature, controller, pool and I/O; - a replacement record with an empty serial also took the old disk's serial. The registry keys disks on serial, so the new disk became the old disk's canonical resource and SMART metric series, and every later poll carried the borrowed serial forward. Current Proxmox reports a missing serial as the literal `unknown`, which the poller records as reported, so the serial copy needs a producer that sends an empty serial, such as the host-agent fallback rows. The evidence copy happens with real Proxmox data. ## Change - `previousPhysicalDiskEvidence` matches by hardware identity through `diskinventory.HardwareIdentityMatch` (serial first, then WWN, each against both fields of the earlier record), and refuses a slot match when both records carry a serial or WWN and none agrees. Missing identity is still not a conflict: placeholders such as `unknown`, and an empty serial, keep the slot's evidence as before. - The registry's merged disk view keeps the host agent's WWN, which the agent spells as unpadded NAA 5 fields (`5-c50-a1b2c3d4`, `internal/hostagent/smartctl.go` `formatWWN`) while Proxmox reports `0x5000c500a1b2c3d4`. `HardwareIdentityMatch` now re-pads that spelling for WWN fields only, so one disk seen by both reporters is not taken for a replacement. The registry's linked-disk join uses the same comparison. ## Tests - `TestPhysicalDiskReplacementInSameSlotDoesNotInheritPreviousSerial` drives the poller and registry across two polls after a swap; on base the replacement inherits the old serial and canonical resource. Its controls keep a serial-omitting same disk on its resource. - `TestPreviousPhysicalDiskEvidenceRejectsSlotMatchAcrossConflictingIdentity` covers renamed devices matched only by identity, PVE vs agent WWN spellings, SAS address serials with a matching WWN, placeholders, and conflicting serial/WWN by source ID and by device token. - Negative controls via `go test -overlay`: on base the replacement, conflict and renamed-device cases fail; with the guard but without the WWN re-padding, the same-disk PVE/agent WWN cases fail. - `pkg/diskinventory`, `internal/unifiedresources` and `internal/monitoring` pass in full. Follow-ups, not in this change: the host-agent SMART merge's device-path fallback can still fill or promote a serial from a stale SMART row; QEMU default serials (`drive-scsi0`) collapse nested Proxmox nodes' disks into one resource; and the registry merges disks across parents on a canonical-ID collision.
This commit is contained in:
@@ -93,14 +93,52 @@ func normalizeHardwareID(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// normalizeHardwareWWN is normalizeHardwareID for a value reported as a WWN.
|
||||
// The host agent also spells smartctl's NAA 5 WWN as unpadded naa-oui-id hex
|
||||
// fields, which only a WWN field can carry, so only WWNs are re-padded.
|
||||
func normalizeHardwareWWN(value string) string {
|
||||
value = normalizeHardwareID(value)
|
||||
if expanded, ok := expandNAA5FieldWWN(value); ok {
|
||||
return expanded
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// expandNAA5FieldWWN rewrites the host agent's "5-c50-a1b2c3d4" spelling of a
|
||||
// NAA 5 WWN (4-bit NAA, 24-bit OUI, 36-bit vendor ID, each printed as unpadded
|
||||
// hex) into the 16-digit form udev and PVE report, "5000c500a1b2c3d4".
|
||||
// Anything that does not fit those field widths is left alone.
|
||||
func expandNAA5FieldWWN(value string) (string, bool) {
|
||||
fields := strings.Split(value, "-")
|
||||
if len(fields) != 3 || fields[0] != "5" ||
|
||||
len(fields[1]) > 6 || len(fields[2]) > 9 ||
|
||||
!isLowerHex(fields[1]) || !isLowerHex(fields[2]) {
|
||||
return "", false
|
||||
}
|
||||
return "5" + strings.Repeat("0", 6-len(fields[1])) + fields[1] +
|
||||
strings.Repeat("0", 9-len(fields[2])) + fields[2], true
|
||||
}
|
||||
|
||||
func isLowerHex(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
for _, char := range value {
|
||||
if (char < '0' || char > '9') && (char < 'a' || char > 'f') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HardwareIdentityMatch reports whether two disk observations carry the same
|
||||
// stable hardware identity. Serial and WWN are folded together because
|
||||
// sources disagree on which field holds the durable identifier: PVE reports a
|
||||
// RAID array volume's NAA identifier as its serial while smartctl reports the
|
||||
// same value as a naa.-prefixed WWN with no serial at all.
|
||||
func HardwareIdentityMatch(leftSerial, leftWWN, rightSerial, rightWWN string) bool {
|
||||
left := [2]string{normalizeHardwareID(leftSerial), normalizeHardwareID(leftWWN)}
|
||||
right := [2]string{normalizeHardwareID(rightSerial), normalizeHardwareID(rightWWN)}
|
||||
left := [2]string{normalizeHardwareID(leftSerial), normalizeHardwareWWN(leftWWN)}
|
||||
right := [2]string{normalizeHardwareID(rightSerial), normalizeHardwareWWN(rightWWN)}
|
||||
for _, l := range left {
|
||||
if l == "" {
|
||||
continue
|
||||
|
||||
@@ -87,6 +87,32 @@ func TestHardwareIdentityMatch(t *testing.T) {
|
||||
bWWN: "0025385b91501234",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// internal/hostagent formatWWN spells smartctl's NAA 5 triple
|
||||
// with unpadded fields; PVE reports udev's ID_WWN.
|
||||
name: "agent naa-oui-id wwn matches pve 0x wwn",
|
||||
aWWN: "5-c50-a1b2c3d4",
|
||||
bWWN: "0x5000c500a1b2c3d4",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "agent naa-oui-id wwn of another disk stays distinct",
|
||||
aWWN: "5-c50-a1b2c3d5",
|
||||
bWWN: "0x5000c500a1b2c3d4",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "serial shaped like the field form is not re-padded",
|
||||
aSerial: "5-c50-a1b2c3d4",
|
||||
bWWN: "0x5000c500a1b2c3d4",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "field form wider than NAA 5 is not re-padded",
|
||||
aWWN: "5-c50-123456789abc",
|
||||
bWWN: "0x5000c50123456789abc",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "truncated udev wwn never matches full sibling identifier",
|
||||
aWWN: "0x61866da053481f00",
|
||||
|
||||
Reference in New Issue
Block a user