feat(models): resolve CPU serial from source PPIN as common fallback

Centralize CPU identity in models.ResolveCPUSerialNumber: an explicit
source serial wins, otherwise a valid source PPIN is used, placeholders
rejected. Dell, H3C and Redfish apply it while parsing; canonical-device
and Reanimator conversion apply it again at the output boundary. No
identity is synthesized from socket/model/board serial.

See ADL-058.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-27 17:05:40 +03:00
co-authored by Claude Sonnet 5
parent 9d701885da
commit 07c270cda2
13 changed files with 122 additions and 19 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ func TestExportCSV_IncludesAllComponentTypesWithUsableSerials(t *testing.T) {
Manufacturer: "Supermicro",
},
CPUs: []models.CPU{
{Socket: 0, Model: "Xeon", SerialNumber: "CPU-001"},
{Socket: 0, Model: "Xeon", PPIN: "D46E5D6B1D3E40E1"},
},
Memory: []models.MemoryDIMM{
{Slot: "DIMM0", PartNumber: "MEM-PN", SerialNumber: "MEM-001", Manufacturer: "Samsung"},
@@ -73,7 +73,7 @@ func TestExportCSV_IncludesAllComponentTypesWithUsableSerials(t *testing.T) {
}
}
want := []string{"FRU-001", "BOARD-001", "CPU-001", "MEM-001", "SSD-001", "GPU-001", "PCIE-001", "NIC-001", "LNIC-001", "PSU-001"}
want := []string{"FRU-001", "BOARD-001", "D46E5D6B1D3E40E1", "MEM-001", "SSD-001", "GPU-001", "PCIE-001", "NIC-001", "LNIC-001", "PSU-001"}
for _, sn := range want {
if !serials[sn] {
t.Fatalf("expected serial %s in csv export", sn)
+7 -4
View File
@@ -113,12 +113,13 @@ func buildDevicesFromLegacy(hw *models.HardwareConfig) []models.HardwareDevice {
details := mergeDetailMaps(nil, cpu.Details)
details = mergeDetailMaps(details, map[string]any{
"socket": cpu.Socket,
"ppin": cpu.PPIN,
})
appendDevice(models.HardwareDevice{
Kind: models.DeviceKindCPU,
Slot: fmt.Sprintf("CPU%d", cpu.Socket),
Model: cpu.Model,
SerialNumber: cpu.SerialNumber,
SerialNumber: models.ResolveCPUSerialNumber(cpu.SerialNumber, cpu.PPIN),
Cores: cpu.Cores,
Threads: cpu.Threads,
FrequencyMHz: cpu.FrequencyMHz,
@@ -837,7 +838,10 @@ func convertCPUsFromDevices(devices []models.HardwareDevice, collectedAt, boardS
UncorrectableErrorCount: int64FromDetailMap(d.Details, "uncorrectable_error_count"),
LifeRemainingPct: floatFromDetailMap(d.Details, "life_remaining_pct"),
LifeUsedPct: floatFromDetailMap(d.Details, "life_used_pct"),
SerialNumber: strings.TrimSpace(d.SerialNumber),
SerialNumber: models.ResolveCPUSerialNumber(
d.SerialNumber,
stringFromDetailMap(d.Details, "ppin"),
),
Firmware: firstNonEmptyString(
stringFromDetailMap(d.Details, "microcode"),
microcodeBySocket[socket],
@@ -1529,7 +1533,7 @@ func convertCPUs(cpus []models.CPU, collectedAt string) []ReanimatorCPU {
Threads: cpu.Threads,
FrequencyMHz: cpu.FrequencyMHz,
MaxFrequencyMHz: cpu.MaxFreqMHz,
SerialNumber: strings.TrimSpace(cpu.SerialNumber),
SerialNumber: models.ResolveCPUSerialNumber(cpu.SerialNumber, cpu.PPIN),
Firmware: "",
Manufacturer: manufacturer,
Status: cpuStatus,
@@ -2793,4 +2797,3 @@ func inferTargetHost(targetHost, filename string) string {
return ""
}
@@ -320,6 +320,31 @@ func TestConvertToReanimator_CPUSerialIsNotSynthesizedAndSocketIsDeduped(t *test
}
}
func TestConvertToReanimator_CPUSerialUsesPPINFallback(t *testing.T) {
input := &models.AnalysisResult{
Hardware: &models.HardwareConfig{
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
Devices: []models.HardwareDevice{{
Kind: models.DeviceKindCPU,
Slot: "CPU0",
Model: "Intel Xeon",
Details: map[string]any{
"socket": 0,
"ppin": "D46E5D6B1D3E40E1",
},
}},
},
}
out, err := ConvertToReanimator(input)
if err != nil {
t.Fatalf("ConvertToReanimator() failed: %v", err)
}
if len(out.Hardware.CPUs) != 1 || out.Hardware.CPUs[0].SerialNumber != "D46E5D6B1D3E40E1" {
t.Fatalf("expected CPU serial fallback from PPIN, got %+v", out.Hardware.CPUs)
}
}
func TestConvertToReanimator_ExportsEventLogsAndOmitsPCIeBDFJSON(t *testing.T) {
input := &models.AnalysisResult{
Filename: "events.json",