From e10440ae325c7a9d292332659f34965d6d91b55b Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Sun, 12 Apr 2026 12:42:58 +0300 Subject: [PATCH] fix(redfish): collect PCIe link width from xFusion servers xFusion iBMC exposes PCIe link width in two non-standard ways: - PCIeInterface uses "Maxlanes" (lowercase 'l') instead of "MaxLanes" - PCIeFunction docs carry width/speed in Oem.xFusion.LinkWidth ("X8"), Oem.xFusion.LinkWidthAbility, Oem.xFusion.LinkSpeed, and Oem.xFusion.LinkSpeedAbility rather than the standard CurrentLinkWidth int Add redfishEnrichFromOEMxFusionPCIeLink and parseXFusionLinkWidth helpers, apply them as fallbacks in NIC and PCIeDevice enrichment paths. Co-Authored-By: Claude Sonnet 4.6 --- internal/collector/redfish.go | 64 +++++++++++++++++++++++++++++- internal/collector/redfish_test.go | 42 ++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/internal/collector/redfish.go b/internal/collector/redfish.go index 647f99c..54d823b 100644 --- a/internal/collector/redfish.go +++ b/internal/collector/redfish.go @@ -3592,7 +3592,7 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter { } if pcieIf, ok := ctrl["PCIeInterface"].(map[string]interface{}); ok && linkWidth == 0 && maxLinkWidth == 0 && linkSpeed == "" && maxLinkSpeed == "" { linkWidth = asInt(pcieIf["LanesInUse"]) - maxLinkWidth = asInt(pcieIf["MaxLanes"]) + maxLinkWidth = firstNonZeroInt(asInt(pcieIf["MaxLanes"]), asInt(pcieIf["Maxlanes"])) linkSpeed = firstNonEmpty(asString(pcieIf["PCIeType"]), asString(pcieIf["CurrentLinkSpeedGTs"]), asString(pcieIf["CurrentLinkSpeed"])) maxLinkSpeed = firstNonEmpty(asString(pcieIf["MaxPCIeType"]), asString(pcieIf["MaxLinkSpeedGTs"]), asString(pcieIf["MaxLinkSpeed"])) } @@ -3705,6 +3705,9 @@ func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{ if strings.TrimSpace(nic.MaxLinkSpeed) == "" { nic.MaxLinkSpeed = firstNonEmpty(asString(pcieDoc["MaxLinkSpeedGTs"]), asString(pcieDoc["MaxLinkSpeed"])) } + if nic.LinkWidth == 0 || nic.MaxLinkWidth == 0 || nic.LinkSpeed == "" || nic.MaxLinkSpeed == "" { + redfishEnrichFromOEMxFusionPCIeLink(pcieDoc, &nic.LinkWidth, &nic.MaxLinkWidth, &nic.LinkSpeed, &nic.MaxLinkSpeed) + } if normalizeRedfishIdentityField(nic.SerialNumber) == "" { nic.SerialNumber = findFirstNormalizedStringByKeys(pcieDoc, "SerialNumber") } @@ -3736,6 +3739,9 @@ func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{ if strings.TrimSpace(nic.MaxLinkSpeed) == "" { nic.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"])) } + if nic.LinkWidth == 0 || nic.MaxLinkWidth == 0 || nic.LinkSpeed == "" || nic.MaxLinkSpeed == "" { + redfishEnrichFromOEMxFusionPCIeLink(fn, &nic.LinkWidth, &nic.MaxLinkWidth, &nic.LinkSpeed, &nic.MaxLinkSpeed) + } if normalizeRedfishIdentityField(nic.SerialNumber) == "" { nic.SerialNumber = findFirstNormalizedStringByKeys(fn, "SerialNumber") } @@ -4384,6 +4390,9 @@ func parsePCIeDeviceWithSupplementalDocs(doc map[string]interface{}, functionDoc if dev.MaxLinkSpeed == "" { dev.MaxLinkSpeed = firstNonEmpty(asString(fn["MaxLinkSpeedGTs"]), asString(fn["MaxLinkSpeed"])) } + if dev.LinkWidth == 0 || dev.MaxLinkWidth == 0 || dev.LinkSpeed == "" || dev.MaxLinkSpeed == "" { + redfishEnrichFromOEMxFusionPCIeLink(fn, &dev.LinkWidth, &dev.MaxLinkWidth, &dev.LinkSpeed, &dev.MaxLinkSpeed) + } } if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) { dev.DeviceClass = firstNonEmpty(redfishFirstStringAcrossDocs(supplementalDocs, "DeviceType"), dev.DeviceClass) @@ -4633,6 +4642,59 @@ func buildBDFfromOemPublic(doc map[string]interface{}) string { return fmt.Sprintf("%04x:%02x:%02x.%x", segment, bus, dev, fn) } +// redfishEnrichFromOEMxFusionPCIeLink fills in missing PCIe link width/speed +// from the xFusion OEM namespace. xFusion reports link width as a string like +// "X8" in Oem.xFusion.LinkWidth / Oem.xFusion.LinkWidthAbility, and link speed +// as a string like "Gen4 (16.0GT/s)" in Oem.xFusion.LinkSpeed / +// Oem.xFusion.LinkSpeedAbility. These fields appear on PCIeFunction docs. +func redfishEnrichFromOEMxFusionPCIeLink(doc map[string]interface{}, linkWidth, maxLinkWidth *int, linkSpeed, maxLinkSpeed *string) { + oem, _ := doc["Oem"].(map[string]interface{}) + if oem == nil { + return + } + xf, _ := oem["xFusion"].(map[string]interface{}) + if xf == nil { + return + } + if *linkWidth == 0 { + *linkWidth = parseXFusionLinkWidth(asString(xf["LinkWidth"])) + } + if *maxLinkWidth == 0 { + *maxLinkWidth = parseXFusionLinkWidth(asString(xf["LinkWidthAbility"])) + } + if strings.TrimSpace(*linkSpeed) == "" { + *linkSpeed = strings.TrimSpace(asString(xf["LinkSpeed"])) + } + if strings.TrimSpace(*maxLinkSpeed) == "" { + *maxLinkSpeed = strings.TrimSpace(asString(xf["LinkSpeedAbility"])) + } +} + +// parseXFusionLinkWidth converts an xFusion link-width string like "X8" or +// "x16" to the integer lane count. Returns 0 for unrecognised values. +func parseXFusionLinkWidth(s string) int { + s = strings.TrimSpace(s) + if s == "" { + return 0 + } + s = strings.TrimPrefix(strings.ToUpper(s), "X") + v := asInt(s) + if v <= 0 { + return 0 + } + return v +} + +// firstNonZeroInt returns the first argument that is non-zero. +func firstNonZeroInt(vals ...int) int { + for _, v := range vals { + if v != 0 { + return v + } + } + return 0 +} + func normalizeRedfishIdentityField(v string) string { v = strings.TrimSpace(v) if v == "" { diff --git a/internal/collector/redfish_test.go b/internal/collector/redfish_test.go index 0e51920..bae1ef8 100644 --- a/internal/collector/redfish_test.go +++ b/internal/collector/redfish_test.go @@ -1341,6 +1341,48 @@ func TestParseNIC_PrefersControllerSlotLabelAndPCIeInterface(t *testing.T) { } } +func TestParseNIC_xFusionMaxlanesAndOEMLinkWidth(t *testing.T) { + // xFusion uses "Maxlanes" (lowercase 'l') in PCIeInterface, not "MaxLanes". + // xFusion also stores per-function link width as Oem.xFusion.LinkWidth = "X8". + nic := parseNIC(map[string]interface{}{ + "Id": "OCPCard1", + "Model": "ConnectX-6 Lx", + "Controllers": []interface{}{ + map[string]interface{}{ + "PCIeInterface": map[string]interface{}{ + "LanesInUse": 8, + "Maxlanes": 8, // xFusion uses lowercase 'l' + "PCIeType": "Gen4", + "MaxPCIeType": "Gen4", + }, + }, + }, + }) + if nic.LinkWidth != 8 || nic.MaxLinkWidth != 8 { + t.Fatalf("expected link widths 8/8 from xFusion Maxlanes, got current=%d max=%d", nic.LinkWidth, nic.MaxLinkWidth) + } + + // enrichNICFromPCIe: OEM xFusion LinkWidth on a PCIeFunction doc. + nic2 := models.NetworkAdapter{} + fnDoc := map[string]interface{}{ + "Oem": map[string]interface{}{ + "xFusion": map[string]interface{}{ + "LinkWidth": "X8", + "LinkWidthAbility": "X8", + "LinkSpeed": "Gen4 (16.0GT/s)", + "LinkSpeedAbility": "Gen4 (16.0GT/s)", + }, + }, + } + enrichNICFromPCIe(&nic2, map[string]interface{}{}, []map[string]interface{}{fnDoc}, nil) + if nic2.LinkWidth != 8 || nic2.MaxLinkWidth != 8 { + t.Fatalf("expected link width 8 from xFusion OEM LinkWidth, got current=%d max=%d", nic2.LinkWidth, nic2.MaxLinkWidth) + } + if nic2.LinkSpeed != "Gen4 (16.0GT/s)" || nic2.MaxLinkSpeed != "Gen4 (16.0GT/s)" { + t.Fatalf("expected link speed from xFusion OEM LinkSpeed, got current=%q max=%q", nic2.LinkSpeed, nic2.MaxLinkSpeed) + } +} + func TestParseNIC_DropsUnrealisticPortCount(t *testing.T) { nic := parseNIC(map[string]interface{}{ "Id": "1",