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
+34 -34
View File
@@ -141,7 +141,7 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
if looksLikeGPU(doc, functionDocs) {
continue
}
if replayPCIeDeviceBackedByCanonicalNIC(doc, functionDocs) {
if r.replayPCIeDeviceBackedByCanonicalNIC(doc, functionDocs) {
continue
}
supplementalDocs := r.getLinkedSupplementalDocs(doc, "EnvironmentMetrics", "Metrics")
@@ -150,7 +150,7 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
supplementalDocs = append(supplementalDocs, r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")...)
}
dev := parsePCIeDeviceWithSupplementalDocs(doc, functionDocs, supplementalDocs)
if shouldSkipReplayPCIeDevice(doc, dev) {
if r.shouldSkipReplayPCIeDevice(doc, dev) {
continue
}
out = append(out, dev)
@@ -164,7 +164,7 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
for idx, fn := range functionDocs {
supplementalDocs := r.getLinkedSupplementalDocs(fn, "EnvironmentMetrics", "Metrics")
dev := parsePCIeFunctionWithSupplementalDocs(fn, supplementalDocs, idx+1)
if shouldSkipReplayPCIeDevice(fn, dev) {
if r.shouldSkipReplayPCIeDevice(fn, dev) {
continue
}
out = append(out, dev)
@@ -173,11 +173,11 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
return dedupePCIeDevices(out)
}
func shouldSkipReplayPCIeDevice(doc map[string]interface{}, dev models.PCIeDevice) bool {
func (r redfishSnapshotReader) shouldSkipReplayPCIeDevice(doc map[string]interface{}, dev models.PCIeDevice) bool {
if isUnidentifiablePCIeDevice(dev) {
return true
}
if replayNetworkFunctionBackedByCanonicalNIC(doc, dev) {
if r.replayNetworkFunctionBackedByCanonicalNIC(doc, dev) {
return true
}
if isReplayStorageServiceEndpoint(doc, dev) {
@@ -192,23 +192,48 @@ func shouldSkipReplayPCIeDevice(doc map[string]interface{}, dev models.PCIeDevic
return false
}
func replayPCIeDeviceBackedByCanonicalNIC(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
func (r redfishSnapshotReader) replayPCIeDeviceBackedByCanonicalNIC(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
if !looksLikeReplayNetworkPCIeDevice(doc, functionDocs) {
return false
}
for _, fn := range functionDocs {
if hasRedfishLinkedMember(fn, "NetworkDeviceFunctions") {
if r.hasResolvableLinkedMember(fn, "NetworkDeviceFunctions") {
return true
}
}
return false
}
func replayNetworkFunctionBackedByCanonicalNIC(doc map[string]interface{}, dev models.PCIeDevice) bool {
func (r redfishSnapshotReader) replayNetworkFunctionBackedByCanonicalNIC(doc map[string]interface{}, dev models.PCIeDevice) bool {
if !looksLikeReplayNetworkClass(dev.DeviceClass) {
return false
}
return hasRedfishLinkedMember(doc, "NetworkDeviceFunctions")
return r.hasResolvableLinkedMember(doc, "NetworkDeviceFunctions")
}
// hasResolvableLinkedMember reports whether the resource(s) linked under
// doc.Links[key] were actually captured in the snapshot. A Links reference
// alone is not enough: some BMCs (e.g. xFusion) advertise linked
// NetworkAdapters/NetworkDeviceFunctions resources whose IDs contain
// characters (like parentheses) that 404 when fetched, so the "canonical"
// NIC never makes it into the snapshot even though the link exists. In that
// case the PCIe device carrying the NIC's hardware identity must not be
// dropped, or the NIC disappears from the inventory entirely.
func (r redfishSnapshotReader) hasResolvableLinkedMember(doc map[string]interface{}, key string) bool {
links, ok := doc["Links"].(map[string]interface{})
if !ok {
return false
}
linked, ok := links[key]
if !ok {
return false
}
for _, path := range extractODataIDs(linked) {
if _, err := r.getJSON(path); err == nil {
return true
}
}
return false
}
func looksLikeReplayNetworkPCIeDevice(doc map[string]interface{}, functionDocs []map[string]interface{}) bool {
@@ -250,31 +275,6 @@ func isReplayStorageServiceEndpoint(doc map[string]interface{}, dev models.PCIeD
return false
}
func hasRedfishLinkedMember(doc map[string]interface{}, key string) bool {
links, ok := doc["Links"].(map[string]interface{})
if !ok {
return false
}
if asInt(links[key+"@odata.count"]) > 0 {
return true
}
linked, ok := links[key]
if !ok {
return false
}
switch v := linked.(type) {
case []interface{}:
return len(v) > 0
case map[string]interface{}:
if asString(v["@odata.id"]) != "" {
return true
}
return len(v) > 0
default:
return false
}
}
func isReplayNoisePCIeClass(class string) bool {
switch strings.ToLower(strings.TrimSpace(class)) {
case "bridge", "processor", "signalprocessingcontroller", "signal processing controller", "serialbuscontroller", "serial bus controller":