fix: parse Inspur PCIe CPU affinity

This commit is contained in:
Mikhail Chusavitin
2026-09-14 10:48:47 +03:00
parent 9ac38c2914
commit 8794e0adec
6 changed files with 140 additions and 54 deletions
+25 -20
View File
@@ -40,6 +40,9 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
// Parse RESTful Network Adapter info
parseNetworkAdapterInfo(text, hw)
// Combined component.log dumps may be the only PCIe inventory source.
hw.PCIeDevices = MergePCIeDevices(hw.PCIeDevices, ParsePCIeDevices(content))
// Extract firmware from all components
extractComponentFirmware(text, hw)
}
@@ -517,25 +520,26 @@ type NetworkAdapterRESTInfo struct {
}
type sysAdapter struct {
ID int `json:"id"`
Name string `json:"name"`
Location string `json:"Location"`
Present int `json:"present"`
Slot int `json:"slot"`
PcieBus int `json:"pcie_bus"`
PcieDev int `json:"pcie_dev"`
PcieFunc int `json:"pcie_func"`
VendorID int `json:"vendor_id"`
DeviceID int `json:"device_id"`
Vendor string `json:"vendor"`
Model string `json:"model"`
FwVer string `json:"fw_ver"`
Status string `json:"status"`
SN string `json:"sn"`
PN string `json:"pn"`
PortNum int `json:"port_num"`
PortType string `json:"port_type"`
Ports []struct {
ID int `json:"id"`
Name string `json:"name"`
Location string `json:"Location"`
DeviceLocator string `json:"location"`
Present int `json:"present"`
Slot int `json:"slot"`
PcieBus int `json:"pcie_bus"`
PcieDev int `json:"pcie_dev"`
PcieFunc int `json:"pcie_func"`
VendorID int `json:"vendor_id"`
DeviceID int `json:"device_id"`
Vendor string `json:"vendor"`
Model string `json:"model"`
FwVer string `json:"fw_ver"`
Status string `json:"status"`
SN string `json:"sn"`
PN string `json:"pn"`
PortNum int `json:"port_num"`
PortType string `json:"port_type"`
Ports []struct {
ID int `json:"id"`
MacAddr string `json:"mac_addr"`
} `json:"ports"`
@@ -579,7 +583,7 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
}
base := models.NetworkAdapter{
Location: adapter.Location,
Location: inspurFirstNonEmpty(adapter.Location, adapter.DeviceLocator),
Present: adapter.Present == 1,
Model: model,
Vendor: vendor,
@@ -591,6 +595,7 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
PortCount: adapter.PortNum,
PortType: adapter.PortType,
Status: adapter.Status,
NUMANode: parseInspurCPUAffinity(adapter.Location, adapter.DeviceLocator),
}
// If another source (asset.json PcieInfo) already recorded this card's PCI
+21
View File
@@ -49,6 +49,27 @@ RESTful fan`
if got.Vendor == "" {
t.Fatalf("expected NIC vendor resolved from pci.ids")
}
if got.NUMANode == nil || *got.NUMANode != 0 {
t.Fatalf("expected CPU affinity 0, got %v", got.NUMANode)
}
}
func TestParseComponentLog_AddsPCIeInventoryAndAffinity(t *testing.T) {
text := `RESTful PCIE Device info:
[{"id":1,"present":1,"vendor_id":6987,"vendor_name":"Marvell","device_id":37424,"device_name":"Marvell 9230 Raid","bus_num":101,"dev_num":0,"func_num":0,"max_link_width":2,"max_link_speed":2,"current_link_width":2,"current_link_speed":2,"location":"#CPU0_PE3_P4_R4_SL1","DeviceLocator":"CPU0_PE3_PCIE4","dev_type":1,"dev_subtype":6}]
RESTful Network Adapter info:
{"sys_adapters":[]}
RESTful fan info:
{"fans":[]}`
hw := &models.HardwareConfig{}
ParseComponentLog([]byte(text), hw)
if len(hw.PCIeDevices) != 1 {
t.Fatalf("expected 1 PCIe device, got %d", len(hw.PCIeDevices))
}
if got := hw.PCIeDevices[0].NUMANode; got == nil || *got != 0 {
t.Fatalf("expected CPU affinity 0, got %v", got)
}
}
func TestParseNetworkAdapterInfo_MergesIntoExistingInventory(t *testing.T) {
+1 -1
View File
@@ -16,7 +16,7 @@ import (
// parserVersion - version of this parser module
// IMPORTANT: Increment this version when making changes to parser logic!
const parserVersion = "2.6"
const parserVersion = "3.0"
func init() {
parser.Register(&Parser{})
+29 -31
View File
@@ -65,20 +65,15 @@ func ParsePCIeSlotDeviceNames(content []byte) map[int]string {
func parsePCIeRESTJSON(content []byte) (PCIeRESTInfo, bool) {
text := string(content)
startMarker := "RESTful PCIE Device info:"
endMarker := "BMC sdr Info:"
startIdx := strings.Index(text, startMarker)
if startIdx == -1 {
return nil, false
}
endIdx := strings.Index(text[startIdx:], endMarker)
if endIdx == -1 {
endIdx = len(text) - startIdx
}
jsonText := strings.TrimSpace(text[startIdx+len(startMarker) : startIdx+endIdx])
jsonText := strings.TrimSpace(text[startIdx+len(startMarker):])
var info PCIeRESTInfo
if err := json.Unmarshal([]byte(jsonText), &info); err != nil {
if err := json.NewDecoder(strings.NewReader(jsonText)).Decode(&info); err != nil {
return nil, false
}
return info, true
@@ -118,27 +113,8 @@ func ParsePCIeNVMeLocToSlot(content []byte) map[int]int {
// ParsePCIeDevices parses RESTful PCIE Device info from devicefrusdr.log
func ParsePCIeDevices(content []byte) []models.PCIeDevice {
text := string(content)
// Find RESTful PCIE Device info section
startMarker := "RESTful PCIE Device info:"
endMarker := "BMC sdr Info:"
startIdx := strings.Index(text, startMarker)
if startIdx == -1 {
return nil
}
endIdx := strings.Index(text[startIdx:], endMarker)
if endIdx == -1 {
endIdx = len(text) - startIdx
}
jsonText := text[startIdx+len(startMarker) : startIdx+endIdx]
jsonText = strings.TrimSpace(jsonText)
var pcieInfo PCIeRESTInfo
if err := json.Unmarshal([]byte(jsonText), &pcieInfo); err != nil {
pcieInfo, ok := parsePCIeRESTJSON(content)
if !ok {
return nil
}
@@ -156,8 +132,9 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
deviceClass := determineDeviceClass(pcie.DevType, pcie.DevSubtype, pcie.DeviceName)
_, pciDeviceName := pciids.DeviceInfo(pcie.VendorID, pcie.DeviceID)
// Build BDF string in canonical form (bb:dd.f)
bdf := formatBDF(pcie.BusNum, pcie.DevNum, pcie.FuncNum)
// Use the domain-qualified BDF as the canonical slot identity. Keep the
// physical locator as descriptive evidence for affinity parsing.
bdf := fullBDF(pcie.BusNum, pcie.DevNum, pcie.FuncNum)
partNumber := strings.TrimSpace(pcie.PartNum)
if partNumber == "" {
@@ -177,7 +154,8 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
}
device := models.PCIeDevice{
Slot: pcie.Location,
Slot: bdf,
Description: strings.TrimSpace(pcie.Location),
VendorID: pcie.VendorID,
DeviceID: pcie.DeviceID,
BDF: bdf,
@@ -190,6 +168,7 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
PartNumber: partNumber,
SerialNumber: strings.TrimSpace(pcie.SerialNum),
Status: pcieRESTStatus(pcie.Status),
NUMANode: parseInspurCPUAffinity(pcie.Location, pcie.DeviceLocator),
}
devices = append(devices, device)
@@ -198,6 +177,22 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
return devices
}
var inspurCPUAffinityRegex = regexp.MustCompile(`(?i)(?:^|[^a-z0-9])CPU(\d+)(?:[^0-9]|$)`)
func parseInspurCPUAffinity(locations ...string) *int {
for _, location := range locations {
match := inspurCPUAffinityRegex.FindStringSubmatch(strings.TrimSpace(location))
if match == nil {
continue
}
value, err := strconv.Atoi(match[1])
if err == nil {
return &value
}
}
return nil
}
// pcieRESTStatus maps the RESTful PCIE Device info "status" flag (1 = OK) to
// the shared status vocabulary. Only the observed OK case is mapped — the
// meaning of other values isn't confirmed in the source, so it's left
@@ -329,6 +324,9 @@ func enrichPCIeDevice(dst *models.PCIeDevice, src models.PCIeDevice) {
if dst.Present == nil {
dst.Present = src.Present
}
if dst.NUMANode == nil {
dst.NUMANode = src.NUMANode
}
if strings.TrimSpace(dst.Status) == "" {
dst.Status = src.Status
}
+39 -2
View File
@@ -19,8 +19,34 @@ BMC sdr Info:`)
if devices[0].PartNumber != "I350T4V2" {
t.Fatalf("expected part/model I350T4V2, got %q", devices[0].PartNumber)
}
if devices[0].BDF != "45:00.0" {
t.Fatalf("expected BDF 45:00.0, got %q", devices[0].BDF)
if devices[0].BDF != "0000:45:00.0" || devices[0].Slot != devices[0].BDF {
t.Fatalf("expected canonical BDF slot 0000:45:00.0, got slot=%q bdf=%q", devices[0].Slot, devices[0].BDF)
}
if devices[0].NUMANode == nil || *devices[0].NUMANode != 0 {
t.Fatalf("expected CPU affinity 0, got %v", devices[0].NUMANode)
}
}
func TestParsePCIeDevices_ReadsAffinityFromCombinedComponentLog(t *testing.T) {
content := []byte(`RESTful PCIE Device info:
[{"id":3,"present":1,"vendor_id":5555,"vendor_name":"NVIDIA","device_id":4125,"device_name":"MCX623106AN-CDAT","bus_num":177,"dev_num":0,"func_num":0,"max_link_width":16,"max_link_speed":4,"current_link_width":16,"current_link_speed":4,"location":"Riser 3 Slot 1","DeviceLocator":"CPU1_PE1_PCIE1","dev_type":2,"dev_subtype":0}]
RESTful Network Adapter info:
{"sys_adapters":[]}`)
devices := ParsePCIeDevices(content)
if len(devices) != 1 {
t.Fatalf("expected 1 device, got %d", len(devices))
}
if devices[0].NUMANode == nil || *devices[0].NUMANode != 1 {
t.Fatalf("expected CPU affinity 1 from DeviceLocator, got %v", devices[0].NUMANode)
}
}
func TestParseInspurCPUAffinity_UnknownRemainsNil(t *testing.T) {
for _, location := range []string{"", "N/A", "Riser 2 Slot 1", "SCPU1_SLOT"} {
if got := parseInspurCPUAffinity(location); got != nil {
t.Fatalf("parseInspurCPUAffinity(%q) = %v, want nil", location, got)
}
}
}
@@ -56,6 +82,17 @@ func TestMergePCIeDevices_EnrichesGenericAssetEntry(t *testing.T) {
}
}
func TestMergePCIeDevices_EnrichesNUMAAffinity(t *testing.T) {
node := 1
got := MergePCIeDevices(
[]models.PCIeDevice{{BDF: "98:00.0"}},
[]models.PCIeDevice{{BDF: "98:00.0", NUMANode: &node}},
)
if len(got) != 1 || got[0].NUMANode == nil || *got[0].NUMANode != 1 {
t.Fatalf("expected merged NUMA affinity 1, got %#v", got)
}
}
func TestParsePCIeDevices_ResolvesModelFromPCIIDsWhenDeviceNameIsRawHex(t *testing.T) {
content := []byte(`RESTful PCIE Device info:
[{"id":5,"present":1,"vendor_id":36869,"vendor_name":"","device_id":655,"device_name":"0x028F","bus_num":152,"dev_num":0,"func_num":0,"max_link_width":8,"max_link_speed":3,"current_link_width":8,"current_link_speed":3,"location":"#CPU1_PCIE9","dev_type":1,"dev_subtype":7,"part_num":"","serial_num":"","fw_ver":""}]