fix(pcie): stop misclassifying same-vendor non-GPU devices as GPUs

matchesGPUVendor (sat_overlay.go) matched any PCIe DeviceClass containing
the substring "Controller" instead of the actual GPU classes, so a
same-vendor non-GPU device (e.g. an NVIDIA-branded NIC/storage controller)
with a degraded PCIe link surfaced as a pcie:gpu:<vendor> alarm on the
Hardware Summary/webui.

isGPUDevice (app_format.go) had the same bug via a different path: an
"any NVIDIA-vendor device is a GPU" fallback that could inflate the
"GPU: N x <model>" audit summary line with non-GPU companion devices
(NVSwitch/NVLink bridges, etc).

Both now defer to collector.IsGPUClass (exported from the previously
unexported isGPUClass), the canonical exact-match classifier already used
by amdgpu.go — removing the duplicated/incorrect class checks instead of
reimplementing them. Also dedup two more locally-duplicated classifiers
within webui (which doesn't import collector by convention): NIC-class
matching (pages.go/page_topo.go) and GPU-class matching in
page_validate.go now share one copy per package instead of being
reimplemented per file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-17 12:50:50 +03:00
co-authored by Claude Sonnet 5
parent 4ac9863353
commit e3697c0a11
10 changed files with 92 additions and 28 deletions
+4 -7
View File
@@ -54,14 +54,11 @@ func topoCard(title, body string) string {
// instead of importing the package for one classifier).
// ---------------------------------------------------------------------------
// isNICDeviceClassDev mirrors the classification logic in hwDescribeNIC
// (pages.go), applied to a single device instead of aggregated counts.
// isNICDeviceClassDev applies isNICDeviceClass (pages.go) to a single device,
// with a MAC-address fallback for devices lspci doesn't classify as NIC.
func isNICDeviceClassDev(dev schema.HardwarePCIeDevice) bool {
if dev.DeviceClass != nil {
c := strings.ToLower(strings.TrimSpace(*dev.DeviceClass))
if c == "ethernetcontroller" || c == "networkcontroller" || strings.Contains(c, "fibrechannel") {
return true
}
if dev.DeviceClass != nil && isNICDeviceClass(*dev.DeviceClass) {
return true
}
return len(dev.MacAddresses) > 0
}
+1 -2
View File
@@ -627,8 +627,7 @@ func validateIsVendorGPU(dev schema.HardwarePCIeDevice, vendor string) bool {
if dev.VendorID != nil && *dev.VendorID == pciVendorAspeed {
return false
}
class := strings.ToLower(validateTrimPtr(dev.DeviceClass))
isGPUClass := class == "videocontroller" || class == "processingaccelerator" || class == "displaycontroller"
isGPUClass := isGPUDeviceClass(validateTrimPtr(dev.DeviceClass))
switch vendor {
case "nvidia":
return isGPUClass && dev.VendorID != nil && *dev.VendorID == pciVendorNvidia
+10 -5
View File
@@ -560,16 +560,21 @@ func hwDescribePSU(hw schema.HardwareSnapshot) string {
return fmt.Sprintf("%d× PSU", n)
}
// isNICDeviceClass reports whether a normalized PCIe DeviceClass string
// (see collector.mapPCIeDeviceClass) identifies a NIC/HBA-class device.
// webui does not import collector (see the "Classification helpers" note
// in page_topo.go), so this mirrors collector.isNICClass locally.
func isNICDeviceClass(class string) bool {
c := strings.ToLower(strings.TrimSpace(class))
return c == "ethernetcontroller" || c == "networkcontroller" || strings.Contains(c, "fibrechannel")
}
// hwDescribeNIC returns a summary like "2× Mellanox ConnectX-6".
func hwDescribeNIC(hw schema.HardwareSnapshot) string {
counts := map[string]int{}
order := []string{}
for _, dev := range hw.PCIeDevices {
isNIC := false
if dev.DeviceClass != nil {
c := strings.ToLower(strings.TrimSpace(*dev.DeviceClass))
isNIC = c == "ethernetcontroller" || c == "networkcontroller" || strings.Contains(c, "fibrechannel")
}
isNIC := dev.DeviceClass != nil && isNICDeviceClass(*dev.DeviceClass)
if !isNIC && len(dev.MacAddresses) == 0 {
continue
}