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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1e4ec513e1
commit
6599ab49c2
@@ -4552,6 +4552,52 @@ func parsePCIeFunctionWithSupplementalDocs(doc map[string]interface{}, supplemen
|
|||||||
return dev
|
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 {
|
func isMissingOrRawPCIModel(model string) bool {
|
||||||
model = strings.TrimSpace(model)
|
model = strings.TrimSpace(model)
|
||||||
if model == "" {
|
if model == "" {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func ReplayRedfishFromRawPayloads(rawPayloads map[string]any, emit ProgressFn) (
|
|||||||
psus := r.collectPSUs(chassisPaths)
|
psus := r.collectPSUs(chassisPaths)
|
||||||
licenses := r.collectLicenses()
|
licenses := r.collectLicenses()
|
||||||
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
pcieDevices := r.collectPCIeDevices(systemPaths, chassisPaths)
|
||||||
|
pcieDevices = append(pcieDevices, r.collectStorageControllers(primarySystem)...)
|
||||||
boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
|
boardInfo := parseBoardInfoWithFallback(systemDoc, chassisDoc, fruDoc)
|
||||||
applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
|
applyBoardInfoFallbackFromDocs(&boardInfo, boardFallbackDocs)
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,45 @@ func (r redfishSnapshotReader) collectStorage(systemPath string, plan redfishpro
|
|||||||
return dedupeStorage(out)
|
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 {
|
func (r redfishSnapshotReader) collectStorageVolumes(systemPath string, plan redfishprofile.ResolvedAnalysisPlan) []models.StorageVolume {
|
||||||
var out []models.StorageVolume
|
var out []models.StorageVolume
|
||||||
storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage"))
|
storageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/Storage"))
|
||||||
|
|||||||
@@ -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) {
|
func TestFirmwareInventoryDeviceName_PrefersIDForGenericSoftwareInventory(t *testing.T) {
|
||||||
doc := map[string]interface{}{
|
doc := map[string]interface{}{
|
||||||
"Id": "HGX_FW_NVSwitch_0",
|
"Id": "HGX_FW_NVSwitch_0",
|
||||||
|
|||||||
Reference in New Issue
Block a user