From 19d857b45904e8fbd167be0737b74685f772928a Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Wed, 4 Mar 2026 22:08:02 +0300 Subject: [PATCH] redfish: filter PCIe topology noise, deduplicate GPU/NIC cross-sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isUnidentifiablePCIeDevice: skip PCIe entries with generic class (SingleFunction/MultiFunction) and no model/serial/VendorID — eliminates PCH bridges, root ports and other bus infrastructure that MSI BMC enumerates exhaustively (59→9 entries on CG480-S5063) - collectPCIeDevices: skip entries where looksLikeGPU — prevents GPU devices from appearing in both hw.GPUs and hw.PCIeDevices (fixed Inspur H100 duplicate) - dedupeCanonicalDevices: secondary model+manufacturer match for noKey items (no serial, no BDF) — merges NetworkAdapter entries into matching PCIe device entries; isGenericDeviceClass helper for DeviceClass identity check (fixed Inspur ENFI1100-T4 duplicate) Co-Authored-By: Claude Sonnet 4.6 --- internal/collector/redfish.go | 27 ++++++++++ internal/collector/redfish_replay.go | 6 +++ internal/exporter/reanimator_converter.go | 62 ++++++++++++++++++++++- 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/internal/collector/redfish.go b/internal/collector/redfish.go index c3b6158..f1e123e 100644 --- a/internal/collector/redfish.go +++ b/internal/collector/redfish.go @@ -720,7 +720,13 @@ func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http. for _, doc := range memberDocs { functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc) + if looksLikeGPU(doc, functionDocs) { + continue + } dev := parsePCIeDevice(doc, functionDocs) + if isUnidentifiablePCIeDevice(dev) { + continue + } out = append(out, dev) } } @@ -2975,6 +2981,27 @@ func isMissingOrRawPCIModel(model string) bool { return false } +// isUnidentifiablePCIeDevice returns true for PCIe topology entries that carry no +// useful inventory information: generic class (SingleFunction/MultiFunction), no +// resolved model or serial, and no PCI vendor/device IDs for future resolution. +// These are typically PCH bridges, root ports, or other bus infrastructure that +// some BMCs (e.g. MSI) enumerate exhaustively in their PCIeDevices collection. +func isUnidentifiablePCIeDevice(dev models.PCIeDevice) bool { + if !isGenericPCIeClassLabel(dev.DeviceClass) { + return false + } + if normalizeRedfishIdentityField(dev.PartNumber) != "" { + return false + } + if normalizeRedfishIdentityField(dev.SerialNumber) != "" { + return false + } + if dev.VendorID > 0 || dev.DeviceID > 0 { + return false + } + return true +} + func isGenericPCIeClassLabel(v string) bool { switch strings.ToLower(strings.TrimSpace(v)) { case "", "pcie device", "display", "display controller", "vga", "3d controller", "network", "network controller", "storage", "storage controller", "other", "unknown", "singlefunction", "multifunction", "simulated": diff --git a/internal/collector/redfish_replay.go b/internal/collector/redfish_replay.go index 5247cc7..ba06320 100644 --- a/internal/collector/redfish_replay.go +++ b/internal/collector/redfish_replay.go @@ -1261,7 +1261,13 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st } for _, doc := range memberDocs { functionDocs := r.getLinkedPCIeFunctions(doc) + if looksLikeGPU(doc, functionDocs) { + continue + } dev := parsePCIeDevice(doc, functionDocs) + if isUnidentifiablePCIeDevice(dev) { + continue + } out = append(out, dev) } } diff --git a/internal/exporter/reanimator_converter.go b/internal/exporter/reanimator_converter.go index 82f1038..9791396 100644 --- a/internal/exporter/reanimator_converter.go +++ b/internal/exporter/reanimator_converter.go @@ -317,11 +317,55 @@ func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevi prev.score = canonicalScore(prev.item) byKey[key] = prev } - out := make([]models.HardwareDevice, 0, len(order)+len(noKey)) + // Secondary pass: for items without serial/BDF (noKey), try to merge into an + // existing keyed entry with the same model+manufacturer. This handles the case + // where a device appears both in PCIeDevices (with BDF) and NetworkAdapters + // (without BDF) — e.g. Inspur outboardPCIeCard vs PCIeCard with the same model. + // deviceIdentity returns the best available model name for secondary matching, + // preferring Model over DeviceClass (which may hold a resolved device name). + deviceIdentity := func(d models.HardwareDevice) string { + if m := strings.ToLower(strings.TrimSpace(d.Model)); m != "" { + return m + } + if dc := strings.ToLower(strings.TrimSpace(d.DeviceClass)); dc != "" && !isGenericDeviceClass(dc) { + return dc + } + return "" + } + + var unmatched []models.HardwareDevice + for _, item := range noKey { + identity := deviceIdentity(item) + mfr := strings.ToLower(strings.TrimSpace(item.Manufacturer)) + if identity == "" { + unmatched = append(unmatched, item) + continue + } + matchKey := "" + matchCount := 0 + for _, k := range order { + existing := byKey[k].item + if deviceIdentity(existing) == identity && + strings.ToLower(strings.TrimSpace(existing.Manufacturer)) == mfr { + matchKey = k + matchCount++ + } + } + if matchCount == 1 { + prev := byKey[matchKey] + prev.item = mergeCanonicalDevice(prev.item, item) + prev.score = canonicalScore(prev.item) + byKey[matchKey] = prev + } else { + unmatched = append(unmatched, item) + } + } + + out := make([]models.HardwareDevice, 0, len(order)+len(unmatched)) for _, key := range order { out = append(out, byKey[key].item) } - out = append(out, noKey...) + out = append(out, unmatched...) for i := range out { out[i].ID = out[i].Kind + ":" + strconv.Itoa(i) } @@ -1371,6 +1415,20 @@ func isGenericPCIeModel(model string) bool { } } +// isGenericDeviceClass returns true for Redfish topology class labels that are +// not meaningful device identifiers (e.g. "SingleFunction", "DisplayController"). +func isGenericDeviceClass(dc string) bool { + switch strings.ToLower(strings.TrimSpace(dc)) { + case "", "pcie device", "display", "display controller", "displaycontroller", + "vga", "3d controller", "network", "network controller", "network adapter", + "storage", "storage controller", "other", "unknown", + "singlefunction", "multifunction", "simulated": + return true + default: + return false + } +} + // inferCPUManufacturer determines CPU manufacturer from model string func inferCPUManufacturer(model string) string { upper := strings.ToUpper(model)