fix(collector): recover GPUs/NICs dropped on xFusion G5500 Redfish exports

looksLikeGPU now falls back to resolving VendorId/DeviceId through the
pci.ids database when the BMC leaves Name/Model/Manufacturer/ClassCode
empty, so GPUs identifiable only by raw PCI IDs (e.g. NVIDIA H100 SXM5
0x10de/0x2330) are no longer misclassified as generic PCIe devices.

The replay pipeline's "backed by canonical NIC" dedup used to trust a
PCIeDevice's Links.NetworkDeviceFunctions reference at face value and
drop the device, assuming a NetworkAdapters record existed elsewhere.
On BMCs that expose resource IDs with characters (parentheses) that
404 on fetch, that canonical NIC never gets captured, so the device
carrying its actual hardware identity vanished from the export
entirely. hasResolvableLinkedMember now verifies the linked resource
is actually present in the snapshot before treating it as authoritative.

Also normalize PartNumber through normalizeRedfishIdentityField in the
GPU/PCIe parsers so a BMC-supplied literal "null" string doesn't leak
into exports verbatim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-18 14:17:25 +03:00
co-authored by Claude Sonnet 5
parent 2c3072cf10
commit a3567dd5f6
3 changed files with 146 additions and 36 deletions
+84
View File
@@ -2639,6 +2639,12 @@ func TestReplayCollectPCIeDevices_SkipsNICsAlreadyRepresentedAsNetworkAdapters(t
"NetworkDeviceFunctions@odata.count": 1,
},
},
// The linked NetworkDeviceFunctions resource was actually captured in
// the snapshot, so the canonical NIC is genuinely available and this
// PCIe duplicate should be skipped.
"/redfish/v1/Chassis/1/NetworkAdapters/NIC1/NetworkDeviceFunctions/Function0": map[string]interface{}{
"Id": "Function0",
},
}}
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
@@ -2647,6 +2653,60 @@ func TestReplayCollectPCIeDevices_SkipsNICsAlreadyRepresentedAsNetworkAdapters(t
}
}
// TestReplayCollectPCIeDevices_KeepsNICWhenCanonicalLinkIsUnresolvable covers
// xFusion BMCs (e.g. G5500 V7) that advertise a Links.NetworkDeviceFunctions
// reference on a PCIeDevice's Function, but whose target resource ID contains
// characters (parentheses) the BMC 404s on when fetched directly. Because
// that referenced resource was never actually captured in the snapshot, the
// PCIe device is the only surviving copy of the NIC's hardware identity and
// must not be dropped, or the NIC vanishes from the inventory entirely.
func TestReplayCollectPCIeDevices_KeepsNICWhenCanonicalLinkIsUnresolvable(t *testing.T) {
r := redfishSnapshotReader{tree: map[string]interface{}{
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
"Members": []interface{}{
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/OCPCard1"},
},
},
"/redfish/v1/Chassis/1/PCIeDevices/OCPCard1": map[string]interface{}{
"Id": "OCPCard1",
"Name": "OCPCard1",
"CardManufacturer": "XFUSION",
"CardModel": "XC385",
"Manufacturer": "XFUSION",
"SerialNumber": "02Y238X6RC000058",
"PCIeFunctions": map[string]interface{}{
"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/OCPCard1/Functions",
},
},
"/redfish/v1/Chassis/1/PCIeDevices/OCPCard1/Functions": map[string]interface{}{
"Members": []interface{}{
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/OCPCard1/Functions/1"},
},
},
"/redfish/v1/Chassis/1/PCIeDevices/OCPCard1/Functions/1": map[string]interface{}{
"DeviceClass": "NetworkController",
"VendorId": "0x15b3",
"DeviceId": "0x101f",
"Links": map[string]interface{}{
"NetworkDeviceFunctions": []interface{}{
map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/NetworkAdapters/MainboardOCPCard1(XC385)/NetworkDeviceFunctions/1"},
},
"NetworkDeviceFunctions@odata.count": 1,
},
},
// Deliberately no tree entry for the linked NetworkDeviceFunctions
// resource — it 404'd during collection and was never captured.
}}
got := r.collectPCIeDevices(nil, []string{"/redfish/v1/Chassis/1"})
if len(got) != 1 {
t.Fatalf("expected the NIC's PCIe device to be kept when its canonical link is unresolvable, got %+v", got)
}
if got[0].SerialNumber != "02Y238X6RC000058" {
t.Fatalf("expected surviving NIC PCIe device, got %+v", got[0])
}
}
func TestReplayCollectPCIeDevices_SkipsStorageServiceEndpoints(t *testing.T) {
r := redfishSnapshotReader{tree: map[string]interface{}{
"/redfish/v1/Chassis/1/PCIeDevices": map[string]interface{}{
@@ -3824,6 +3884,30 @@ func TestLooksLikeGPU_NVSwitchExcluded(t *testing.T) {
}
}
// TestLooksLikeGPU_ResolvesVendorDeviceIDWhenModelTextMissing covers xFusion
// BMCs (e.g. G5500 V7) whose PCIeDevice doc and linked PCIeFunction leave
// Name/Model/Manufacturer/ClassCode empty but expose raw VendorId/DeviceId
// (0x10de/0x2330 = NVIDIA H100 SXM5). Without a pci.ids lookup these devices
// silently fall through to the generic pcie_devices list instead of gpus.
func TestLooksLikeGPU_ResolvesVendorDeviceIDWhenModelTextMissing(t *testing.T) {
doc := map[string]interface{}{
"Id": "PCIeCard1",
"Name": "PCIeCard1",
"Model": nil,
"Manufacturer": nil,
}
functionDocs := []map[string]interface{}{
{
"DeviceClass": "Other",
"VendorId": "0x10de",
"DeviceId": "0x2330",
},
}
if !looksLikeGPU(doc, functionDocs) {
t.Fatal("expected NVIDIA H100 (0x10de/0x2330) to be classified as a GPU via VendorId/DeviceId resolution")
}
}
func TestFirmwareInventoryDeviceName_PrefersIDForGenericSoftwareInventory(t *testing.T) {
doc := map[string]interface{}{
"Id": "HGX_FW_NVSwitch_0",