Files
pulse/pkg/diskinventory/identity_test.go
T
rcourtman 9b644b3d49 Stop a replaced PVE disk inheriting the old disk's evidence
The disk poller keeps evidence it could not collect this poll by looking
up the previous record, first by serial and WWN, then by the Proxmox
source ID and device token. Those last two 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 serial. The registry keys disks on serial, so the
new disk became the old disk's canonical resource and SMART metric
series, and each later poll carried the borrowed serial forward.

Match on hardware identity through diskinventory.HardwareIdentityMatch,
serial first then WWN, and refuse a slot match when both records carry
a serial or WWN that share no value. Missing identity is still not a
conflict, so Proxmox's literal "unknown" and other placeholders 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") while Proxmox
reports "0x5000c500a1b2c3d4". HardwareIdentityMatch now re-pads that
spelling for WWN fields, so the same disk seen by both reporters is
not mistaken for a replacement.
2026-10-07 11:23:34 +01:00

154 lines
4.6 KiB
Go

package diskinventory
import "testing"
func TestPreferredIDPreservesDirectDeviceFallbacks(t *testing.T) {
for _, test := range []struct {
name string
device string
controller string
target string
want string
}{
{name: "sata", device: "/dev/sda", want: "host:sda"},
{name: "nvme", device: "nvme0n1", want: "host:nvme0n1"},
{name: "direct sas hctl", device: "sdb", controller: "0000:03:00.0", target: "6:0:0:0", want: "host:sdb"},
{name: "controller member", device: "sdc [megaraid,7]", controller: "sdc", target: "megaraid,7", want: "host:sdc@sdc/megaraid,7"},
{name: "areca member", device: "sdc [areca,1/1]", controller: "arcmsr0", target: "areca,1/1", want: "host:sdc@arcmsr0/areca,1/1"},
{name: "sssraid member", device: "sg2 [sssraid,0,1]", controller: "sssraid0", target: "sssraid,0,1", want: "host:sg2@sssraid0/sssraid,0,1"},
} {
t.Run(test.name, func(t *testing.T) {
got := PreferredID("", "", "host", test.device, test.controller, test.target)
if got != test.want {
t.Fatalf("PreferredID() = %q, want %q", got, test.want)
}
})
}
}
func TestPreferredIDKeepsExistingHardwareIdentityPriority(t *testing.T) {
if got := PreferredID(" SERIAL ", "WWN", "host", "sda", "controller", "megaraid,7"); got != "SERIAL" {
t.Fatalf("serial identity = %q, want SERIAL", got)
}
if got := PreferredID("", " WWN ", "host", "sda", "controller", "megaraid,7"); got != "WWN" {
t.Fatalf("WWN identity = %q, want WWN", got)
}
}
func TestPreferredIDRejectsPlaceholderHardwareIdentity(t *testing.T) {
for _, placeholder := range []string{
"UNKNOWN",
"N/A",
"DEFAULT-SERIAL",
"0000-0000-0000",
"FFFF:FFFF",
} {
if IsUsableHardwareID(placeholder) {
t.Fatalf("placeholder %q was treated as usable hardware identity", placeholder)
}
if got := PreferredID(placeholder, "", "host-a", "/dev/sda", "", ""); got != "host-a:sda" {
t.Fatalf("placeholder %q produced ID %q, want scoped fallback", placeholder, got)
}
}
if !IsUsableHardwareID("ZR5DLAYJ") || !IsUsableHardwareID("naa.5000c500abcdef01") {
t.Fatal("real disk serial/WWN was rejected")
}
}
func TestHardwareIdentityMatch(t *testing.T) {
cases := []struct {
name string
aSerial, aWWN string
bSerial, bWWN string
want bool
}{
{
name: "pve bare-hex serial matches smartctl naa wwn",
aSerial: "61866da053481f002f58a43b22f964a7",
aWWN: "0x61866da053481f00",
bWWN: "naa.61866da053481f002f58a43b22f964a7",
want: true,
},
{
name: "same serial different case",
aSerial: "zr5dlayj",
bSerial: "ZR5DLAYJ",
want: true,
},
{
name: "udev wwn-0x token matches naa wwn",
aWWN: "wwn-0x5000c500abcdef01",
bWWN: "naa.5000c500abcdef01",
want: true,
},
{
name: "eui prefix matches bare nvme id",
aWWN: "eui.0025385b91501234",
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",
bSerial: "61866da053481f0030543ecb1d3b4cca",
bWWN: "naa.61866da053481f0030543ecb1d3b4cca",
want: false,
},
{
name: "placeholder serials do not match each other",
aSerial: "UNKNOWN",
bSerial: "UNKNOWN",
want: false,
},
{
name: "distinct disks stay distinct",
aSerial: "9410A0FWFVL9",
bSerial: "35C0A39YFVL9",
want: false,
},
{
name: "empty observations never match",
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := HardwareIdentityMatch(tc.aSerial, tc.aWWN, tc.bSerial, tc.bWWN)
if got != tc.want {
t.Fatalf("HardwareIdentityMatch(%q,%q,%q,%q) = %v, want %v",
tc.aSerial, tc.aWWN, tc.bSerial, tc.bWWN, got, tc.want)
}
mirrored := HardwareIdentityMatch(tc.bSerial, tc.bWWN, tc.aSerial, tc.aWWN)
if mirrored != tc.want {
t.Fatalf("match is not symmetric: mirrored = %v, want %v", mirrored, tc.want)
}
})
}
}