unraid: parse dimm/nic/pcie and annotate duplicate serials

This commit is contained in:
2026-03-01 18:14:45 +03:00
parent 7d1a02cb72
commit 206496efae
4 changed files with 653 additions and 10 deletions

View File

@@ -300,7 +300,7 @@ func BuildHardwareDevices(hw *models.HardwareConfig) []models.HardwareDevice {
})
}
return dedupeDevices(all)
return annotateDuplicateSerials(dedupeDevices(all))
}
func isEmptyPCIeDevice(p models.PCIeDevice) bool {
@@ -443,9 +443,22 @@ func shouldMergeDevices(a, b models.HardwareDevice) bool {
bSN := strings.ToLower(normalizedSerial(b.SerialNumber))
aBDF := strings.ToLower(strings.TrimSpace(a.BDF))
bBDF := strings.ToLower(strings.TrimSpace(b.BDF))
aSlot := normalizeSlot(a.Slot)
bSlot := normalizeSlot(b.Slot)
// Memory DIMMs can legitimately share serial number in some dumps.
// Never merge DIMMs with different slots.
if a.Kind == models.DeviceKindMemory && b.Kind == models.DeviceKindMemory {
if aSlot != "" && bSlot != "" && aSlot != bSlot {
return false
}
}
// Hard conflicts.
if aSN != "" && bSN != "" && aSN == bSN {
if a.Kind == models.DeviceKindMemory && b.Kind == models.DeviceKindMemory {
return aSlot != "" && bSlot != "" && aSlot == bSlot
}
return true
}
if aSN != "" && bSN != "" && aSN != bSN {
@@ -465,7 +478,7 @@ func shouldMergeDevices(a, b models.HardwareDevice) bool {
if hasMACOverlap(a.MACAddresses, b.MACAddresses) {
return true
}
if normalizeSlot(a.Slot) != "" && normalizeSlot(a.Slot) == normalizeSlot(b.Slot) {
if aSlot != "" && aSlot == bSlot {
return true
}
return false
@@ -481,7 +494,7 @@ func shouldMergeDevices(a, b models.HardwareDevice) bool {
if sameManufacturer(a, b) {
score += 2
}
if normalizeSlot(a.Slot) != "" && normalizeSlot(a.Slot) == normalizeSlot(b.Slot) {
if aSlot != "" && aSlot == bSlot {
score += 2
}
if hasMACOverlap(a.MACAddresses, b.MACAddresses) {
@@ -732,3 +745,35 @@ func buildFirmwareBySlot(firmware []models.FirmwareInfo) map[string]slotFirmware
func normalizeSlotKey(slot string) string {
return strings.ToLower(strings.TrimSpace(slot))
}
func annotateDuplicateSerials(items []models.HardwareDevice) []models.HardwareDevice {
if len(items) < 2 {
return items
}
countByKindSerial := make(map[string]int)
for _, d := range items {
serial := normalizedSerial(d.SerialNumber)
if serial == "" {
continue
}
key := d.Kind + "|" + strings.ToLower(serial)
countByKindSerial[key]++
}
seenByKindSerial := make(map[string]int)
for i := range items {
serial := normalizedSerial(items[i].SerialNumber)
if serial == "" {
continue
}
key := items[i].Kind + "|" + strings.ToLower(serial)
if countByKindSerial[key] < 2 {
continue
}
seenByKindSerial[key]++
items[i].SerialNumber = serial + " (DUP#" + strconv.Itoa(seenByKindSerial[key]) + ")"
}
return items
}

View File

@@ -65,6 +65,54 @@ func TestBuildHardwareDevices_SkipsEmptyMemorySlots(t *testing.T) {
}
}
func TestBuildHardwareDevices_MemorySameSerialDifferentSlots_NotDeduped(t *testing.T) {
hw := &models.HardwareConfig{
Memory: []models.MemoryDIMM{
{Slot: "Node0_Dimm1", Location: "Node0_Bank0", Present: true, SizeMB: 16384, SerialNumber: "238F7649", PartNumber: "M393B2G70BH0-"},
{Slot: "Node0_Dimm3", Location: "Node0_Bank0", Present: true, SizeMB: 16384, SerialNumber: "238F7649", PartNumber: "M393B2G70BH0-"},
},
}
devices := BuildHardwareDevices(hw)
memorySlots := make(map[string]bool)
for _, d := range devices {
if d.Kind != models.DeviceKindMemory {
continue
}
memorySlots[d.Slot] = true
}
if len(memorySlots) != 2 {
t.Fatalf("expected 2 memory devices, got %d", len(memorySlots))
}
if !memorySlots["Node0_Dimm1"] || !memorySlots["Node0_Dimm3"] {
t.Fatalf("expected both Node0_Dimm1 and Node0_Dimm3 to remain")
}
}
func TestBuildHardwareDevices_DuplicateSerials_AreAnnotated(t *testing.T) {
hw := &models.HardwareConfig{
Memory: []models.MemoryDIMM{
{Slot: "A1", Location: "BANK0", Present: true, SizeMB: 16384, SerialNumber: "SN-1"},
{Slot: "A2", Location: "BANK1", Present: true, SizeMB: 16384, SerialNumber: "SN-1"},
},
}
devices := BuildHardwareDevices(hw)
var serials []string
for _, d := range devices {
if d.Kind == models.DeviceKindMemory {
serials = append(serials, d.SerialNumber)
}
}
if len(serials) != 2 {
t.Fatalf("expected 2 memory devices, got %d", len(serials))
}
if serials[0] != "SN-1 (DUP#1)" || serials[1] != "SN-1 (DUP#2)" {
t.Fatalf("unexpected annotated serials: %+v", serials)
}
}
func TestBuildHardwareDevices_DedupCrossKindByBDF(t *testing.T) {
hw := &models.HardwareConfig{
PCIeDevices: []models.PCIeDevice{