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:
@@ -720,7 +720,13 @@ func (c *RedfishConnector) collectPCIeDevices(ctx context.Context, client *http.
|
|||||||
|
|
||||||
for _, doc := range memberDocs {
|
for _, doc := range memberDocs {
|
||||||
functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc)
|
functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, doc)
|
||||||
|
if looksLikeGPU(doc, functionDocs) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
dev := parsePCIeDevice(doc, functionDocs)
|
dev := parsePCIeDevice(doc, functionDocs)
|
||||||
|
if isUnidentifiablePCIeDevice(dev) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
out = append(out, dev)
|
out = append(out, dev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2975,6 +2981,27 @@ func isMissingOrRawPCIModel(model string) bool {
|
|||||||
return false
|
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 {
|
func isGenericPCIeClassLabel(v string) bool {
|
||||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
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":
|
case "", "pcie device", "display", "display controller", "vga", "3d controller", "network", "network controller", "storage", "storage controller", "other", "unknown", "singlefunction", "multifunction", "simulated":
|
||||||
|
|||||||
@@ -1261,7 +1261,13 @@ func (r redfishSnapshotReader) collectPCIeDevices(systemPaths, chassisPaths []st
|
|||||||
}
|
}
|
||||||
for _, doc := range memberDocs {
|
for _, doc := range memberDocs {
|
||||||
functionDocs := r.getLinkedPCIeFunctions(doc)
|
functionDocs := r.getLinkedPCIeFunctions(doc)
|
||||||
|
if looksLikeGPU(doc, functionDocs) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
dev := parsePCIeDevice(doc, functionDocs)
|
dev := parsePCIeDevice(doc, functionDocs)
|
||||||
|
if isUnidentifiablePCIeDevice(dev) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
out = append(out, dev)
|
out = append(out, dev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,11 +317,55 @@ func dedupeCanonicalDevices(items []models.HardwareDevice) []models.HardwareDevi
|
|||||||
prev.score = canonicalScore(prev.item)
|
prev.score = canonicalScore(prev.item)
|
||||||
byKey[key] = prev
|
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 {
|
for _, key := range order {
|
||||||
out = append(out, byKey[key].item)
|
out = append(out, byKey[key].item)
|
||||||
}
|
}
|
||||||
out = append(out, noKey...)
|
out = append(out, unmatched...)
|
||||||
for i := range out {
|
for i := range out {
|
||||||
out[i].ID = out[i].Kind + ":" + strconv.Itoa(i)
|
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
|
// inferCPUManufacturer determines CPU manufacturer from model string
|
||||||
func inferCPUManufacturer(model string) string {
|
func inferCPUManufacturer(model string) string {
|
||||||
upper := strings.ToUpper(model)
|
upper := strings.ToUpper(model)
|
||||||
|
|||||||
Reference in New Issue
Block a user