fix: parse Inspur PCIe CPU affinity
This commit is contained in:
@@ -2020,3 +2020,28 @@ couple a standard inventory document to one viewer.
|
|||||||
`reanimator.json`.
|
`reanimator.json`.
|
||||||
- Fan RPM animation, PSU live load, and other Bee live-only presentation are
|
- Fan RPM animation, PSU live load, and other Bee live-only presentation are
|
||||||
intentionally out of scope.
|
intentionally out of scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADL-069 — Inspur physical CPU locator as PCIe affinity
|
||||||
|
|
||||||
|
**Date:** 2026-09-14
|
||||||
|
**Context:** Some Inspur/Kaytus `onekeylog` archives do not contain host sysfs
|
||||||
|
NUMA data, but their RESTful PCIe inventory identifies the physical processor
|
||||||
|
in `location` or `DeviceLocator` values such as `#CPU0_PE2_P7_R5_SL0`.
|
||||||
|
|
||||||
|
**Decision:** Parse the bounded `CPU<n>` token from those locator fields into
|
||||||
|
the nullable `numa_node`/CPU-affinity field for PCIe devices and network
|
||||||
|
adapters. Combined `component.log` is also a valid PCIe inventory source when
|
||||||
|
`devicefrusdr.log` is absent. RESTful PCIe entries use their domain-qualified
|
||||||
|
BDF as canonical `slot`, retaining the physical locator as description, so
|
||||||
|
card/function records merge with the NIC inventory. Locator values without an
|
||||||
|
isolated `CPU<n>` token remain unknown (`nil`); no affinity is inferred from
|
||||||
|
BDF numbering.
|
||||||
|
|
||||||
|
**Consequences:**
|
||||||
|
- Inspur physical PCIe-to-socket wiring can drive the standalone topology even
|
||||||
|
for BMC-only dumps.
|
||||||
|
- `CPU0` remains distinct from missing affinity.
|
||||||
|
- On systems with Sub-NUMA Clustering, the value represents physical CPU
|
||||||
|
affinity rather than an operating-system NUMA-domain ID.
|
||||||
|
|||||||
+6
-1
@@ -40,6 +40,9 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
|||||||
// Parse RESTful Network Adapter info
|
// Parse RESTful Network Adapter info
|
||||||
parseNetworkAdapterInfo(text, hw)
|
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
|
// Extract firmware from all components
|
||||||
extractComponentFirmware(text, hw)
|
extractComponentFirmware(text, hw)
|
||||||
}
|
}
|
||||||
@@ -520,6 +523,7 @@ type sysAdapter struct {
|
|||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Location string `json:"Location"`
|
Location string `json:"Location"`
|
||||||
|
DeviceLocator string `json:"location"`
|
||||||
Present int `json:"present"`
|
Present int `json:"present"`
|
||||||
Slot int `json:"slot"`
|
Slot int `json:"slot"`
|
||||||
PcieBus int `json:"pcie_bus"`
|
PcieBus int `json:"pcie_bus"`
|
||||||
@@ -579,7 +583,7 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
base := models.NetworkAdapter{
|
base := models.NetworkAdapter{
|
||||||
Location: adapter.Location,
|
Location: inspurFirstNonEmpty(adapter.Location, adapter.DeviceLocator),
|
||||||
Present: adapter.Present == 1,
|
Present: adapter.Present == 1,
|
||||||
Model: model,
|
Model: model,
|
||||||
Vendor: vendor,
|
Vendor: vendor,
|
||||||
@@ -591,6 +595,7 @@ func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
|
|||||||
PortCount: adapter.PortNum,
|
PortCount: adapter.PortNum,
|
||||||
PortType: adapter.PortType,
|
PortType: adapter.PortType,
|
||||||
Status: adapter.Status,
|
Status: adapter.Status,
|
||||||
|
NUMANode: parseInspurCPUAffinity(adapter.Location, adapter.DeviceLocator),
|
||||||
}
|
}
|
||||||
|
|
||||||
// If another source (asset.json PcieInfo) already recorded this card's PCI
|
// If another source (asset.json PcieInfo) already recorded this card's PCI
|
||||||
|
|||||||
@@ -49,6 +49,27 @@ RESTful fan`
|
|||||||
if got.Vendor == "" {
|
if got.Vendor == "" {
|
||||||
t.Fatalf("expected NIC vendor resolved from pci.ids")
|
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) {
|
func TestParseNetworkAdapterInfo_MergesIntoExistingInventory(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
// parserVersion - version of this parser module
|
// parserVersion - version of this parser module
|
||||||
// IMPORTANT: Increment this version when making changes to parser logic!
|
// IMPORTANT: Increment this version when making changes to parser logic!
|
||||||
const parserVersion = "2.6"
|
const parserVersion = "3.0"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
parser.Register(&Parser{})
|
parser.Register(&Parser{})
|
||||||
|
|||||||
+29
-31
@@ -65,20 +65,15 @@ func ParsePCIeSlotDeviceNames(content []byte) map[int]string {
|
|||||||
func parsePCIeRESTJSON(content []byte) (PCIeRESTInfo, bool) {
|
func parsePCIeRESTJSON(content []byte) (PCIeRESTInfo, bool) {
|
||||||
text := string(content)
|
text := string(content)
|
||||||
startMarker := "RESTful PCIE Device info:"
|
startMarker := "RESTful PCIE Device info:"
|
||||||
endMarker := "BMC sdr Info:"
|
|
||||||
|
|
||||||
startIdx := strings.Index(text, startMarker)
|
startIdx := strings.Index(text, startMarker)
|
||||||
if startIdx == -1 {
|
if startIdx == -1 {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
endIdx := strings.Index(text[startIdx:], endMarker)
|
jsonText := strings.TrimSpace(text[startIdx+len(startMarker):])
|
||||||
if endIdx == -1 {
|
|
||||||
endIdx = len(text) - startIdx
|
|
||||||
}
|
|
||||||
jsonText := strings.TrimSpace(text[startIdx+len(startMarker) : startIdx+endIdx])
|
|
||||||
|
|
||||||
var info PCIeRESTInfo
|
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 nil, false
|
||||||
}
|
}
|
||||||
return info, true
|
return info, true
|
||||||
@@ -118,27 +113,8 @@ func ParsePCIeNVMeLocToSlot(content []byte) map[int]int {
|
|||||||
|
|
||||||
// ParsePCIeDevices parses RESTful PCIE Device info from devicefrusdr.log
|
// ParsePCIeDevices parses RESTful PCIE Device info from devicefrusdr.log
|
||||||
func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
||||||
text := string(content)
|
pcieInfo, ok := parsePCIeRESTJSON(content)
|
||||||
|
if !ok {
|
||||||
// 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 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +132,9 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
deviceClass := determineDeviceClass(pcie.DevType, pcie.DevSubtype, pcie.DeviceName)
|
deviceClass := determineDeviceClass(pcie.DevType, pcie.DevSubtype, pcie.DeviceName)
|
||||||
_, pciDeviceName := pciids.DeviceInfo(pcie.VendorID, pcie.DeviceID)
|
_, pciDeviceName := pciids.DeviceInfo(pcie.VendorID, pcie.DeviceID)
|
||||||
|
|
||||||
// Build BDF string in canonical form (bb:dd.f)
|
// Use the domain-qualified BDF as the canonical slot identity. Keep the
|
||||||
bdf := formatBDF(pcie.BusNum, pcie.DevNum, pcie.FuncNum)
|
// physical locator as descriptive evidence for affinity parsing.
|
||||||
|
bdf := fullBDF(pcie.BusNum, pcie.DevNum, pcie.FuncNum)
|
||||||
|
|
||||||
partNumber := strings.TrimSpace(pcie.PartNum)
|
partNumber := strings.TrimSpace(pcie.PartNum)
|
||||||
if partNumber == "" {
|
if partNumber == "" {
|
||||||
@@ -177,7 +154,8 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
device := models.PCIeDevice{
|
device := models.PCIeDevice{
|
||||||
Slot: pcie.Location,
|
Slot: bdf,
|
||||||
|
Description: strings.TrimSpace(pcie.Location),
|
||||||
VendorID: pcie.VendorID,
|
VendorID: pcie.VendorID,
|
||||||
DeviceID: pcie.DeviceID,
|
DeviceID: pcie.DeviceID,
|
||||||
BDF: bdf,
|
BDF: bdf,
|
||||||
@@ -190,6 +168,7 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
PartNumber: partNumber,
|
PartNumber: partNumber,
|
||||||
SerialNumber: strings.TrimSpace(pcie.SerialNum),
|
SerialNumber: strings.TrimSpace(pcie.SerialNum),
|
||||||
Status: pcieRESTStatus(pcie.Status),
|
Status: pcieRESTStatus(pcie.Status),
|
||||||
|
NUMANode: parseInspurCPUAffinity(pcie.Location, pcie.DeviceLocator),
|
||||||
}
|
}
|
||||||
|
|
||||||
devices = append(devices, device)
|
devices = append(devices, device)
|
||||||
@@ -198,6 +177,22 @@ func ParsePCIeDevices(content []byte) []models.PCIeDevice {
|
|||||||
return devices
|
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
|
// pcieRESTStatus maps the RESTful PCIE Device info "status" flag (1 = OK) to
|
||||||
// the shared status vocabulary. Only the observed OK case is mapped — the
|
// 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
|
// 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 {
|
if dst.Present == nil {
|
||||||
dst.Present = src.Present
|
dst.Present = src.Present
|
||||||
}
|
}
|
||||||
|
if dst.NUMANode == nil {
|
||||||
|
dst.NUMANode = src.NUMANode
|
||||||
|
}
|
||||||
if strings.TrimSpace(dst.Status) == "" {
|
if strings.TrimSpace(dst.Status) == "" {
|
||||||
dst.Status = src.Status
|
dst.Status = src.Status
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-2
@@ -19,8 +19,34 @@ BMC sdr Info:`)
|
|||||||
if devices[0].PartNumber != "I350T4V2" {
|
if devices[0].PartNumber != "I350T4V2" {
|
||||||
t.Fatalf("expected part/model I350T4V2, got %q", devices[0].PartNumber)
|
t.Fatalf("expected part/model I350T4V2, got %q", devices[0].PartNumber)
|
||||||
}
|
}
|
||||||
if devices[0].BDF != "45:00.0" {
|
if devices[0].BDF != "0000:45:00.0" || devices[0].Slot != devices[0].BDF {
|
||||||
t.Fatalf("expected BDF 45:00.0, got %q", 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) {
|
func TestParsePCIeDevices_ResolvesModelFromPCIIDsWhenDeviceNameIsRawHex(t *testing.T) {
|
||||||
content := []byte(`RESTful PCIE Device info:
|
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":""}]
|
[{"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":""}]
|
||||||
|
|||||||
Reference in New Issue
Block a user