From 7a1285db996fc849f060677401ee4492e9285545 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Tue, 24 Feb 2026 18:25:00 +0300 Subject: [PATCH] Expand Redfish storage fallback for enclosure Disk.Bay paths --- internal/collector/redfish.go | 78 +++++++++++++++++++++++----- internal/collector/redfish_replay.go | 27 +++++++++- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/internal/collector/redfish.go b/internal/collector/redfish.go index 70e5ec2..e9a1805 100644 --- a/internal/collector/redfish.go +++ b/internal/collector/redfish.go @@ -186,6 +186,11 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie for _, driveDoc := range driveDocs { out = append(out, parseDrive(driveDoc)) } + if len(driveDocs) == 0 { + for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, driveCollectionPath) { + out = append(out, parseDrive(driveDoc)) + } + } } continue } @@ -213,6 +218,24 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie if looksLikeDrive(member) { out = append(out, parseDrive(member)) } + + // Supermicro/RAID implementations can expose physical disks under chassis enclosures + // linked from Storage.Links.Enclosures, while Storage.Drives stays empty. + for _, enclosurePath := range redfishLinkRefs(member, "Links", "Enclosures") { + driveDocs, err := c.getCollectionMembers(ctx, client, req, baseURL, joinPath(enclosurePath, "/Drives")) + if err == nil { + for _, driveDoc := range driveDocs { + if looksLikeDrive(driveDoc) { + out = append(out, parseDrive(driveDoc)) + } + } + if len(driveDocs) == 0 { + for _, driveDoc := range c.probeDirectDiskBayChildren(ctx, client, req, baseURL, joinPath(enclosurePath, "/Drives")) { + out = append(out, parseDrive(driveDoc)) + } + } + } + } } // Fallback for platforms that expose disks in SimpleStorage. @@ -615,10 +638,10 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht // Some Supermicro BMCs expose NVMe disks at direct Disk.Bay endpoints even when the // Drives collection returns Members: []. Probe those paths so raw export can be replayed. for path := range out { - if !isSupermicroNVMeBackplanePath(path) { + if !strings.HasSuffix(normalizeRedfishPath(path), "/Drives") { continue } - for _, bayPath := range supermicroNVMeDiskBayCandidates(path) { + for _, bayPath := range directDiskBayCandidates(path) { doc, err := c.getJSON(ctx, client, req, baseURL, bayPath) if err != nil { continue @@ -643,8 +666,21 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht } func (c *RedfishConnector) probeSupermicroNVMeDiskBays(ctx context.Context, client *http.Client, req Request, baseURL, backplanePath string) []map[string]interface{} { + return c.probeDirectDiskBayChildren(ctx, client, req, baseURL, joinPath(backplanePath, "/Drives")) +} + +func isSupermicroNVMeBackplanePath(path string) bool { + path = normalizeRedfishPath(path) + return strings.Contains(path, "/Chassis/NVMeSSD.") && strings.Contains(path, ".StorageBackplane") +} + +func supermicroNVMeDiskBayCandidates(backplanePath string) []string { + return directDiskBayCandidates(joinPath(backplanePath, "/Drives")) +} + +func (c *RedfishConnector) probeDirectDiskBayChildren(ctx context.Context, client *http.Client, req Request, baseURL, drivesCollectionPath string) []map[string]interface{} { var out []map[string]interface{} - for _, path := range supermicroNVMeDiskBayCandidates(backplanePath) { + for _, path := range directDiskBayCandidates(drivesCollectionPath) { doc, err := c.getJSON(ctx, client, req, baseURL, path) if err != nil || !looksLikeDrive(doc) { continue @@ -654,18 +690,36 @@ func (c *RedfishConnector) probeSupermicroNVMeDiskBays(ctx context.Context, clie return out } -func isSupermicroNVMeBackplanePath(path string) bool { - path = normalizeRedfishPath(path) - return strings.Contains(path, "/Chassis/NVMeSSD.") && strings.Contains(path, ".StorageBackplane") -} - -func supermicroNVMeDiskBayCandidates(backplanePath string) []string { - const maxBays = 64 - prefix := joinPath(backplanePath, "/Drives") - out := make([]string, 0, maxBays*2) +func directDiskBayCandidates(drivesCollectionPath string) []string { + const maxBays = 128 + prefix := normalizeRedfishPath(drivesCollectionPath) + out := make([]string, 0, maxBays*3) for i := 0; i < maxBays; i++ { out = append(out, fmt.Sprintf("%s/Disk.Bay.%d", prefix, i)) out = append(out, fmt.Sprintf("%s/Disk.Bay%d", prefix, i)) + out = append(out, fmt.Sprintf("%s/%d", prefix, i)) + } + return out +} + +func redfishLinkRefs(doc map[string]interface{}, topKey, nestedKey string) []string { + top, ok := doc[topKey].(map[string]interface{}) + if !ok { + return nil + } + items, ok := top[nestedKey].([]interface{}) + if !ok { + return nil + } + out := make([]string, 0, len(items)) + for _, itemAny := range items { + item, ok := itemAny.(map[string]interface{}) + if !ok { + continue + } + if p := asString(item["@odata.id"]); p != "" { + out = append(out, p) + } } return out } diff --git a/internal/collector/redfish_replay.go b/internal/collector/redfish_replay.go index 8fe3d78..cb29f1e 100644 --- a/internal/collector/redfish_replay.go +++ b/internal/collector/redfish_replay.go @@ -208,6 +208,11 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag for _, driveDoc := range driveDocs { out = append(out, parseDrive(driveDoc)) } + if len(driveDocs) == 0 { + for _, driveDoc := range r.probeDirectDiskBayChildren(driveCollectionPath) { + out = append(out, parseDrive(driveDoc)) + } + } } continue } @@ -233,6 +238,22 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag if looksLikeDrive(member) { out = append(out, parseDrive(member)) } + + for _, enclosurePath := range redfishLinkRefs(member, "Links", "Enclosures") { + driveDocs, err := r.getCollectionMembers(joinPath(enclosurePath, "/Drives")) + if err == nil { + for _, driveDoc := range driveDocs { + if looksLikeDrive(driveDoc) { + out = append(out, parseDrive(driveDoc)) + } + } + if len(driveDocs) == 0 { + for _, driveDoc := range r.probeDirectDiskBayChildren(joinPath(enclosurePath, "/Drives")) { + out = append(out, parseDrive(driveDoc)) + } + } + } + } } simpleStorageMembers, _ := r.getCollectionMembers(joinPath(systemPath, "/SimpleStorage")) @@ -278,8 +299,12 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag } func (r redfishSnapshotReader) probeSupermicroNVMeDiskBays(backplanePath string) []map[string]interface{} { + return r.probeDirectDiskBayChildren(joinPath(backplanePath, "/Drives")) +} + +func (r redfishSnapshotReader) probeDirectDiskBayChildren(drivesCollectionPath string) []map[string]interface{} { var out []map[string]interface{} - for _, path := range supermicroNVMeDiskBayCandidates(backplanePath) { + for _, path := range directDiskBayCandidates(drivesCollectionPath) { doc, err := r.getJSON(path) if err != nil || !looksLikeDrive(doc) { continue