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:
co-authored by
Claude Sonnet 5
parent
9d701885da
commit
07c270cda2
@@ -3311,6 +3311,7 @@ func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
||||
if serial == "" && publicSerial == "" {
|
||||
serial = findFirstNormalizedStringByKeys(doc, "SerialNumber")
|
||||
}
|
||||
ppin := firstNonEmpty(findFirstNormalizedStringByKeys(doc, "PPIN", "ProtectedIdentificationNumber"), publicSerial)
|
||||
cpus = append(cpus, models.CPU{
|
||||
Socket: socket,
|
||||
Model: firstNonEmpty(asString(doc["Model"]), asString(doc["Name"])),
|
||||
@@ -3318,8 +3319,8 @@ func parseCPUs(docs []map[string]interface{}) []models.CPU {
|
||||
Threads: asInt(doc["TotalThreads"]),
|
||||
FrequencyMHz: int(redfishFirstNumeric(doc, "OperatingSpeedMHz", "CurrentSpeedMHz", "FrequencyMHz")),
|
||||
MaxFreqMHz: int(redfishFirstNumeric(doc, "MaxSpeedMHz", "TurboEnableMaxSpeedMHz", "TurboDisableMaxSpeedMHz")),
|
||||
PPIN: firstNonEmpty(findFirstNormalizedStringByKeys(doc, "PPIN", "ProtectedIdentificationNumber"), publicSerial),
|
||||
SerialNumber: serial,
|
||||
PPIN: ppin,
|
||||
SerialNumber: models.ResolveCPUSerialNumber(serial, ppin),
|
||||
L1CacheKB: l1,
|
||||
L2CacheKB: l2,
|
||||
L3CacheKB: l3,
|
||||
|
||||
@@ -1557,8 +1557,8 @@ func TestParseCPU_UsesPublicSerialAsPPINAndCurrentSpeedMHz(t *testing.T) {
|
||||
if cpus[0].PPIN != "6FB5241E81CECDFD" {
|
||||
t.Fatalf("expected PPIN from Oem.Public.SerialNumber, got %+v", cpus[0])
|
||||
}
|
||||
if cpus[0].SerialNumber != "" {
|
||||
t.Fatalf("expected empty CPU serial number when only Public serial exists, got %+v", cpus[0])
|
||||
if cpus[0].SerialNumber != "6FB5241E81CECDFD" {
|
||||
t.Fatalf("expected CPU serial number to fall back to PPIN, got %+v", cpus[0])
|
||||
}
|
||||
if cpus[0].FrequencyMHz != 2700 {
|
||||
t.Fatalf("expected CPU frequency from Oem.Public.CurrentSpeedMHz, got %+v", cpus[0])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import "strings"
|
||||
|
||||
// ResolveCPUSerialNumber returns a source-backed processor inventory identity.
|
||||
// A vendor-provided serial number has priority; PPIN is the documented CPU
|
||||
// serial identity and is used when a separate serial number is unavailable.
|
||||
func ResolveCPUSerialNumber(serialNumber, ppin string) string {
|
||||
if serial := validCPUIdentity(serialNumber); serial != "" {
|
||||
return serial
|
||||
}
|
||||
return validCPUIdentity(ppin)
|
||||
}
|
||||
|
||||
func validCPUIdentity(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
switch strings.ToLower(value) {
|
||||
case "", "-", "n/a", "na", "none", "null", "unknown", "not available", "to be filled by o.e.m.":
|
||||
return ""
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveCPUSerialNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
serial string
|
||||
ppin string
|
||||
want string
|
||||
}{
|
||||
{name: "serial has priority", serial: "CPU-SERIAL", ppin: "D46E5D6B1D3E40E1", want: "CPU-SERIAL"},
|
||||
{name: "PPIN fallback", ppin: " D46E5D6B1D3E40E1 ", want: "D46E5D6B1D3E40E1"},
|
||||
{name: "placeholder serial falls back", serial: "Unknown", ppin: "D44F8D6B9155EE0E", want: "D44F8D6B9155EE0E"},
|
||||
{name: "placeholder PPIN rejected", ppin: "N/A", want: ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ResolveCPUSerialNumber(tt.serial, tt.ppin); got != tt.want {
|
||||
t.Fatalf("ResolveCPUSerialNumber(%q, %q) = %q, want %q", tt.serial, tt.ppin, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -19,7 +19,7 @@ import (
|
||||
"git.mchus.pro/mchus/logpile/internal/parser/vendors/pciids"
|
||||
)
|
||||
|
||||
const parserVersion = "3.0"
|
||||
const parserVersion = "3.1"
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
@@ -323,6 +323,7 @@ func parseCPUView(props map[string]string, result *models.AnalysisResult) {
|
||||
if model == "" {
|
||||
return
|
||||
}
|
||||
ppin := strings.TrimSpace(props["ppin"])
|
||||
cpu := models.CPU{
|
||||
Socket: parseSocketFromFQDD(firstNonEmpty(props["fqdd"], props["instanceid"])),
|
||||
Model: model,
|
||||
@@ -331,7 +332,8 @@ func parseCPUView(props map[string]string, result *models.AnalysisResult) {
|
||||
Threads: parseIntLoose(props["numberofenabledthreads"]),
|
||||
FrequencyMHz: parseIntLoose(props["currentclockspeed"]),
|
||||
MaxFreqMHz: parseIntLoose(props["maxclockspeed"]),
|
||||
PPIN: strings.TrimSpace(props["ppin"]),
|
||||
PPIN: ppin,
|
||||
SerialNumber: models.ResolveCPUSerialNumber(props["serialnumber"], ppin),
|
||||
Status: normalizeStatus(props["primarystatus"]),
|
||||
}
|
||||
result.Hardware.CPUs = append(result.Hardware.CPUs, cpu)
|
||||
|
||||
+3
@@ -152,6 +152,9 @@ func TestParseNestedTSRZip(t *testing.T) {
|
||||
if got := result.Hardware.CPUs[0].Model; got != "Intel(R) Xeon(R) Gold 6330" {
|
||||
t.Fatalf("unexpected cpu model: %q", got)
|
||||
}
|
||||
if got := result.Hardware.CPUs[0].SerialNumber; got != "ABCD" {
|
||||
t.Fatalf("expected CPU serial fallback from PPIN, got %q", got)
|
||||
}
|
||||
|
||||
if len(result.Hardware.NetworkAdapters) != 1 {
|
||||
t.Fatalf("expected 1 network adapter, got %d", len(result.Hardware.NetworkAdapters))
|
||||
|
||||
+9
-6
@@ -20,8 +20,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
parserVersionG5 = "2.1"
|
||||
parserVersionG6 = "2.1"
|
||||
parserVersionG5 = "2.2"
|
||||
parserVersionG6 = "2.2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -506,6 +506,8 @@ func parseHardwareInfoNetworkAdapters(content []byte) []models.NetworkAdapter {
|
||||
func parseCPUFromHardwareInfoSection(sectionName string, section map[string]string) models.CPU {
|
||||
socket := parseSectionIndex(sectionName, `(?i)processor\s+(\d+)`)
|
||||
statusRaw := getSectionValue(section, "Status")
|
||||
ppin := getSectionValue(section, "CPU PPIN", "PPIN")
|
||||
serial := getSectionValue(section, "Processor ID", "Serial Number")
|
||||
|
||||
return models.CPU{
|
||||
Socket: socket,
|
||||
@@ -516,8 +518,8 @@ func parseCPUFromHardwareInfoSection(sectionName string, section map[string]stri
|
||||
L1CacheKB: parseMaybeIntLoose(getSectionValue(section, "L1 Cache")),
|
||||
L2CacheKB: parseMaybeIntLoose(getSectionValue(section, "L2 Cache")),
|
||||
L3CacheKB: parseMaybeIntLoose(getSectionValue(section, "L3 Cache")),
|
||||
PPIN: getSectionValue(section, "CPU PPIN", "PPIN"),
|
||||
SerialNumber: getSectionValue(section, "Processor ID", "Serial Number"),
|
||||
PPIN: ppin,
|
||||
SerialNumber: models.ResolveCPUSerialNumber(serial, ppin),
|
||||
Status: normalizeComponentStatus(statusRaw),
|
||||
}
|
||||
}
|
||||
@@ -1503,6 +1505,7 @@ func parseCPUXML(content []byte, result *models.AnalysisResult) {
|
||||
}
|
||||
|
||||
socket := parseSocketID(node.XMLName.Local)
|
||||
ppin := strings.TrimSpace(fields["PPIN"])
|
||||
cpu := models.CPU{
|
||||
Socket: socket,
|
||||
Model: model,
|
||||
@@ -1511,8 +1514,8 @@ func parseCPUXML(content []byte, result *models.AnalysisResult) {
|
||||
Threads: parseMaybeInt(fields["TotalThreads"]),
|
||||
FrequencyMHz: parseMaybeInt(fields["ProcessorSpeed"]),
|
||||
MaxFreqMHz: parseMaybeInt(fields["ProcessorMaxSpeed"]),
|
||||
SerialNumber: strings.TrimSpace(fields["SerialNumber"]),
|
||||
PPIN: strings.TrimSpace(fields["PPIN"]),
|
||||
SerialNumber: models.ResolveCPUSerialNumber(fields["SerialNumber"], ppin),
|
||||
PPIN: ppin,
|
||||
Status: normalizePresenceStatus(fields["Status"]),
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -831,6 +831,9 @@ Pre-Init,Info,0x0,Management Subsystem Health,Health,Assertion event,Pre-Init,"M
|
||||
if result.Hardware.CPUs[0].FrequencyMHz != 2800 {
|
||||
t.Fatalf("expected CPU frequency 2800MHz, got %d", result.Hardware.CPUs[0].FrequencyMHz)
|
||||
}
|
||||
if got := result.Hardware.CPUs[0].SerialNumber; got != "49-A9-50-C0-15-9F-2D-DC" {
|
||||
t.Fatalf("expected CPU serial fallback from PPIN, got %q", got)
|
||||
}
|
||||
|
||||
if len(result.Hardware.Memory) != 2 {
|
||||
t.Fatalf("expected 2 DIMMs from hardware_info.ini, got %d", len(result.Hardware.Memory))
|
||||
|
||||
@@ -57,7 +57,7 @@ func BuildHardwareDevices(hw *models.HardwareConfig) []models.HardwareDevice {
|
||||
Source: "cpus",
|
||||
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,
|
||||
|
||||
@@ -315,3 +315,18 @@ func TestHandleGetConfig_ReturnsCanonicalHardware(t *testing.T) {
|
||||
t.Fatalf("did not expect legacy hardware.cpus in config response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHardwareDevices_CPUUsesPPINAsSerialFallback(t *testing.T) {
|
||||
hw := &models.HardwareConfig{CPUs: []models.CPU{{Socket: 0, Model: "Intel Xeon", PPIN: "D46E5D6B1D3E40E1"}}}
|
||||
|
||||
devices := BuildHardwareDevices(hw)
|
||||
for _, device := range devices {
|
||||
if device.Kind == models.DeviceKindCPU {
|
||||
if device.SerialNumber != "D46E5D6B1D3E40E1" {
|
||||
t.Fatalf("expected CPU serial fallback from PPIN, got %q", device.SerialNumber)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("CPU device was not built")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user