From 6599ab49c217c4e7a5d777eaedb9f3d4125021d1 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Tue, 18 Aug 2026 14:30:56 +0300 Subject: [PATCH] fix(collector): surface RAID/HBA controller cards from Storage.StorageControllers[] The RAID controller (XFusion XC170-M-8i / Broadcom SAS3808) never appeared in inventory at all: it isn't listed in any Chassis/Systems PCIeDevices collection on this BMC, and its dedicated Board resource link 404s (id contains parentheses, same class of bug as the OCP NIC fixed earlier). Its full identity -- model, firmware, BDF, vendor/device IDs -- was sitting unread in the Storage resource's embedded StorageControllers[] array the whole time. Added parseStorageControllerPCIeDevice + collectStorageControllers to read that array and surface the controller as a PCIeDevice entry, merged into the existing pcie_devices list. Co-Authored-By: Claude Sonnet 5 --- internal/collector/redfish.go | 46 ++++++++++ internal/collector/redfish_replay.go | 1 + internal/collector/redfish_replay_storage.go | 39 +++++++++ internal/collector/redfish_test.go | 90 ++++++++++++++++++++ 4 files changed, 176 insertions(+) diff --git a/internal/collector/redfish.go b/internal/collector/redfish.go index 73498c9..34c6521 100644 --- a/internal/collector/redfish.go +++ b/internal/collector/redfish.go @@ -4552,6 +4552,52 @@ func parsePCIeFunctionWithSupplementalDocs(doc map[string]interface{}, supplemen return dev } +// parseStorageControllerPCIeDevice builds a PCIeDevice entry from a +// Storage.StorageControllers[] array member (Redfish's dedicated RAID/HBA +// controller resource). Some BMCs (e.g. xFusion) never expose this +// controller as a standalone PCIeDevice/Board resource — its Board link can +// even 404 (id containing parentheses) — so without reading this embedded +// array the RAID controller card never appears in inventory at all, even +// though its identity (model, firmware, BDF, vendor/device IDs) is fully +// present right here. +func parseStorageControllerPCIeDevice(ctrl map[string]interface{}) models.PCIeDevice { + oem := redfishOEMxFusionSection(ctrl) + + dev := models.PCIeDevice{ + Slot: firstNonEmpty( + asString(ctrl["CardModel"]), + asString(ctrl["Name"]), + asString(ctrl["MemberId"]), + ), + BDF: sanitizeRedfishBDF(asString(oem["BDF"])), + DeviceClass: "RAIDController", + Manufacturer: firstNonEmpty(asString(ctrl["Manufacturer"]), asString(ctrl["CardManufacturer"])), + Model: firstNonEmpty(asString(ctrl["CardModel"]), asString(oem["Type"])), + Firmware: asString(ctrl["FirmwareVersion"]), + PartNumber: normalizeRedfishIdentityField(asString(ctrl["PartNumber"])), + SerialNumber: findFirstNormalizedStringByKeys(ctrl, "SerialNumber"), + VendorID: asHexOrInt(oem["VenderID"]), + DeviceID: asHexOrInt(oem["DeviceID"]), + Status: mapStatus(ctrl["Status"]), + } + if dev.Slot == "" { + dev.Slot = "RAIDController" + } + return dev +} + +func redfishOEMxFusionSection(doc map[string]interface{}) map[string]interface{} { + oem, ok := doc["Oem"].(map[string]interface{}) + if !ok { + return nil + } + xfusion, ok := oem["xFusion"].(map[string]interface{}) + if !ok { + return nil + } + return xfusion +} + func isMissingOrRawPCIModel(model string) bool { model = strings.TrimSpace(model) if model == "" { diff --git a/internal/collector/redfish_replay.go b/internal/collector/redfish_replay.go index bad6c9e..a34d4f6 100644 --- a/internal/collector/redfish_replay.go +++ b/internal/collector/redfish_replay.go @@ -81,6 +81,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) ( psus := r.collectPSUs(chassisPaths) licenses := r.collectLicenses() pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths) + pcieDevices = append(pcieDevices, r.collectStorageControllers(primarySystem)...) boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc) applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs) diff --git a/internal/collector/redfish_replay_storage.go b/internal/collector/redfish_replay_storage.go index b744915..f50245c 100644 --- a/internal/collector/redfish_replay_storage.go +++ b/internal/collector/redfish_replay_storage.go @@ -143,6 +143,45 @@ func (r redfishSnapshotReader) collectStorage(systemPath string, plan redfishpro return dedupeStorage(out) } +// collectStorageControllers surfaces RAID/HBA controller cards from the +// Storage.StorageControllers[] embedded array as PCIeDevice inventory +// entries. Some BMCs (e.g. xFusion) never expose the controller as its own +// PCIeDevice or Board resource -- the Board link can even 404 on IDs +// containing parentheses -- so without reading this array the RAID +// controller never appears in inventory even though its model/firmware/BDF +// identity is fully present here. +func (r redfishSnapshotReader) collectStorageControllers(systemPath string) []models.PCIeDevice { + var out []models.PCIeDevice + storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage")) + seen := make(map[string]struct{}) + for _, member := range storageMembers { + controllers, ok := member["StorageControllers"].([]interface{}) + if !ok { + continue + } + for _, ctrlAny := range controllers { + ctrl, ok := ctrlAny.(map[string]interface{}) + if !ok { + continue + } + dev := parseStorageControllerPCIeDevice(ctrl) + if isUnidentifiablePCIeDevice(dev) { + continue + } + key := firstNonEmpty(dev.SerialNumber, dev.BDF, dev.Slot) + if key == "" { + continue + } + if _, dup := seen[key]; dup { + continue + } + seen[key] = struct{}{} + out = append(out, dev) + } + } + return out +} + func (r redfishSnapshotReader) collectStorageVolumes(systemPath string, plan redfishprofile.ResolvedAnalysisPlan) []models.StorageVolume { var out []models.StorageVolume storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage")) diff --git a/internal/collector/redfish_test.go b/internal/collector/redfish_test.go index cc4e17d..db0529d 100644 --- a/internal/collector/redfish_test.go +++ b/internal/collector/redfish_test.go @@ -3978,6 +3978,96 @@ func TestParseGPUWithSupplementalDocs_ResolvesModelWhenOnlySlotLabelPresent(t *t } } +// TestParseStorageControllerPCIeDevice_XFusionRAIDCard reproduces the G5500 +// V7 case: the RAID/HBA controller card is never exposed as its own +// PCIeDevice or Board resource (the Board link 404s on an id containing +// parentheses), but its full identity is embedded in +// Storage.StorageControllers[]. Without reading that array, the RAID +// controller never appears in inventory at all. +func TestParseStorageControllerPCIeDevice_XFusionRAIDCard(t *testing.T) { + ctrl := map[string]interface{}{ + "CardManufacturer": "XFUSION", + "CardModel": "XC170-M-8i", + "Manufacturer": "Broadcom", + "Model": nil, + "Name": "RAID Card1(XC170-M-8i) Controller", + "FirmwareVersion": "5.340.01-4227", + "PartNumber": "0302Y204", + "SerialNumber": nil, + "Status": map[string]interface{}{"State": "Enabled"}, + "Oem": map[string]interface{}{ + "xFusion": map[string]interface{}{ + "BDF": "0000:25:02.0", + "VenderID": "0x1000", + "DeviceID": "0x10e6", + "Type": "SAS3808iMR", + }, + }, + } + dev := parseStorageControllerPCIeDevice(ctrl) + if dev.Slot != "XC170-M-8i" { + t.Errorf("expected slot XC170-M-8i, got %q", dev.Slot) + } + if dev.Manufacturer != "Broadcom" { + t.Errorf("expected manufacturer Broadcom, got %q", dev.Manufacturer) + } + if dev.Model != "XC170-M-8i" { + t.Errorf("expected model XC170-M-8i, got %q", dev.Model) + } + if dev.BDF != "0000:25:02.0" { + t.Errorf("expected BDF 0000:25:02.0, got %q", dev.BDF) + } + if dev.VendorID != 0x1000 || dev.DeviceID != 0x10e6 { + t.Errorf("expected vendor/device 0x1000/0x10e6, got %#x/%#x", dev.VendorID, dev.DeviceID) + } + if dev.PartNumber != "0302Y204" { + t.Errorf("expected part number 0302Y204, got %q", dev.PartNumber) + } + if dev.DeviceClass != "RAIDController" { + t.Errorf("expected device class RAIDController, got %q", dev.DeviceClass) + } +} + +// TestReplayCollectStorageControllers_SurfacesRAIDCardMissingFromPCIeTree +// covers the end-to-end replay path: the RAID controller is only reachable +// via Systems/1/Storage's embedded StorageControllers[] array, never via +// Chassis/Systems PCIeDevices collections (those don't list it on this BMC). +func TestReplayCollectStorageControllers_SurfacesRAIDCardMissingFromPCIeTree(t *testing.T) { + r := redfishSnapshotReader{tree: map[string]interface{}{ + "/redfish/v1/Systems/1/Storage": map[string]interface{}{ + "Members": []interface{}{ + map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storages/RAIDStorage0"}, + }, + }, + "/redfish/v1/Systems/1/Storages/RAIDStorage0": map[string]interface{}{ + "Id": "RAIDStorage0", + "StorageControllers": []interface{}{ + map[string]interface{}{ + "CardManufacturer": "XFUSION", + "CardModel": "XC170-M-8i", + "Manufacturer": "Broadcom", + "PartNumber": "0302Y204", + "Oem": map[string]interface{}{ + "xFusion": map[string]interface{}{ + "BDF": "0000:25:02.0", + "VenderID": "0x1000", + "DeviceID": "0x10e6", + }, + }, + }, + }, + }, + }} + + got := r.collectStorageControllers("/redfish/v1/Systems/1") + if len(got) != 1 { + t.Fatalf("expected exactly 1 RAID controller, got %+v", got) + } + if got[0].Model != "XC170-M-8i" { + t.Fatalf("expected XC170-M-8i, got %+v", got[0]) + } +} + func TestFirmwareInventoryDeviceName_PrefersIDForGenericSoftwareInventory(t *testing.T) { doc := map[string]interface{}{ "Id": "HGX_FW_NVSwitch_0",