redfish: filter PCIe topology noise, deduplicate GPU/NIC cross-sources

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 22:08:02 +03:00
parent 8d80048117
commit 19d857b459
3 changed files with 93 additions and 2 deletions

View File

@@ -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)