From a6c90b6e773f4a6c23d05859502701ab1927bf03 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Tue, 24 Feb 2026 18:22:02 +0300 Subject: [PATCH] Probe Supermicro NVMe Disk.Bay endpoints for drive inventory --- internal/collector/redfish.go | 151 ++++++++++++++++++++++++++- internal/collector/redfish_replay.go | 31 ++++++ internal/collector/redfish_test.go | 121 +++++++++++++++++++++ 3 files changed, 299 insertions(+), 4 deletions(-) diff --git a/internal/collector/redfish.go b/internal/collector/redfish.go index cda77ea..70e5ec2 100644 --- a/internal/collector/redfish.go +++ b/internal/collector/redfish.go @@ -245,6 +245,17 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie out = append(out, parseDrive(driveDoc)) } } + for _, chassisPath := range chassisPaths { + if !isSupermicroNVMeBackplanePath(chassisPath) { + continue + } + for _, driveDoc := range c.probeSupermicroNVMeDiskBays(ctx, client, req, baseURL, chassisPath) { + if !looksLikeDrive(driveDoc) { + continue + } + out = append(out, parseDrive(driveDoc)) + } + } out = dedupeStorage(out) return out @@ -260,6 +271,14 @@ func (c *RedfishConnector) collectNICs(ctx context.Context, client *http.Client, } for _, doc := range adapterDocs { nic := parseNIC(doc) + for _, pciePath := range networkAdapterPCIeDevicePaths(doc) { + pcieDoc, err := c.getJSON(ctx, client, req, baseURL, pciePath) + if err != nil { + continue + } + functionDocs := c.getLinkedPCIeFunctions(ctx, client, req, baseURL, pcieDoc) + enrichNICFromPCIe(&nic, pcieDoc, functionDocs) + } key := firstNonEmpty(nic.SerialNumber, nic.Slot+"|"+nic.Model) if key == "" { continue @@ -593,6 +612,25 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht close(stopHeartbeat) close(jobs) + // 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) { + continue + } + for _, bayPath := range supermicroNVMeDiskBayCandidates(path) { + doc, err := c.getJSON(ctx, client, req, baseURL, bayPath) + if err != nil { + continue + } + if !looksLikeDrive(doc) { + continue + } + out[normalizeRedfishPath(bayPath)] = doc + c.debugSnapshotf("snapshot nvme bay probe hit path=%s", bayPath) + } + } + if emit != nil { emit(Progress{ Status: "running", @@ -604,6 +642,34 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht return out } +func (c *RedfishConnector) probeSupermicroNVMeDiskBays(ctx context.Context, client *http.Client, req Request, baseURL, backplanePath string) []map[string]interface{} { + var out []map[string]interface{} + for _, path := range supermicroNVMeDiskBayCandidates(backplanePath) { + doc, err := c.getJSON(ctx, client, req, baseURL, path) + if err != nil || !looksLikeDrive(doc) { + continue + } + out = append(out, doc) + } + 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) + 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)) + } + return out +} + func shouldCrawlPath(path string) bool { if path == "" { return false @@ -845,10 +911,22 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter { if strings.TrimSpace(vendor) == "" { vendor = pciids.VendorName(vendorID) } + location := redfishLocationLabel(doc["Location"]) + var firmware string + var portCount int + if controllers, ok := doc["Controllers"].([]interface{}); ok && len(controllers) > 0 { + if ctrl, ok := controllers[0].(map[string]interface{}); ok { + location = firstNonEmpty(location, redfishLocationLabel(ctrl["Location"])) + firmware = asString(ctrl["FirmwarePackageVersion"]) + if caps, ok := ctrl["ControllerCapabilities"].(map[string]interface{}); ok { + portCount = asInt(caps["NetworkPortCount"]) + } + } + } return models.NetworkAdapter{ Slot: firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])), - Location: asString(doc["Location"]), + Location: location, Present: !strings.EqualFold(mapStatus(doc["Status"]), "Absent"), Model: strings.TrimSpace(model), Vendor: strings.TrimSpace(vendor), @@ -856,10 +934,70 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter { DeviceID: deviceID, SerialNumber: asString(doc["SerialNumber"]), PartNumber: asString(doc["PartNumber"]), + Firmware: firmware, + PortCount: portCount, Status: mapStatus(doc["Status"]), } } +func networkAdapterPCIeDevicePaths(doc map[string]interface{}) []string { + var out []string + if controllers, ok := doc["Controllers"].([]interface{}); ok { + for _, ctrlAny := range controllers { + ctrl, ok := ctrlAny.(map[string]interface{}) + if !ok { + continue + } + links, ok := ctrl["Links"].(map[string]interface{}) + if !ok { + continue + } + refs, ok := links["PCIeDevices"].([]interface{}) + if !ok { + continue + } + for _, refAny := range refs { + ref, ok := refAny.(map[string]interface{}) + if !ok { + continue + } + if p := asString(ref["@odata.id"]); p != "" { + out = append(out, p) + } + } + } + } + return out +} + +func enrichNICFromPCIe(nic *models.NetworkAdapter, pcieDoc map[string]interface{}, functionDocs []map[string]interface{}) { + if nic == nil { + return + } + if nic.VendorID == 0 { + nic.VendorID = asHexOrInt(pcieDoc["VendorId"]) + } + if nic.DeviceID == 0 { + nic.DeviceID = asHexOrInt(pcieDoc["DeviceId"]) + } + for _, fn := range functionDocs { + if nic.VendorID == 0 { + nic.VendorID = asHexOrInt(fn["VendorId"]) + } + if nic.DeviceID == 0 { + nic.DeviceID = asHexOrInt(fn["DeviceId"]) + } + } + if strings.TrimSpace(nic.Vendor) == "" { + nic.Vendor = pciids.VendorName(nic.VendorID) + } + if isMissingOrRawPCIModel(nic.Model) { + if resolved := pciids.DeviceName(nic.VendorID, nic.DeviceID); resolved != "" { + nic.Model = resolved + } + } +} + func parsePSU(doc map[string]interface{}, idx int) models.PSU { status := mapStatus(doc["Status"]) present := true @@ -969,7 +1107,7 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter dev := models.PCIeDevice{ Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), asString(doc["Name"]), asString(doc["Id"])), BDF: asString(doc["BDF"]), - DeviceClass: firstNonEmpty(asString(doc["DeviceType"]), asString(doc["PCIeType"])), + DeviceClass: asString(doc["DeviceType"]), Manufacturer: asString(doc["Manufacturer"]), PartNumber: asString(doc["PartNumber"]), SerialNumber: asString(doc["SerialNumber"]), @@ -981,7 +1119,7 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter if dev.BDF == "" { dev.BDF = asString(fn["FunctionId"]) } - if dev.DeviceClass == "" { + if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) { dev.DeviceClass = firstNonEmpty(asString(fn["DeviceClass"]), asString(fn["ClassCode"])) } if dev.VendorID == 0 { @@ -1012,6 +1150,11 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter dev.DeviceClass = resolved } } + if isGenericPCIeClassLabel(dev.DeviceClass) { + // Redfish DeviceType (e.g. MultiFunction/Simulated) is a topology attribute, + // not a user-facing device name. Prefer model/part labels when class cannot be resolved. + dev.DeviceClass = firstNonEmpty(asString(doc["Model"]), dev.PartNumber, dev.DeviceClass) + } if strings.TrimSpace(dev.Manufacturer) == "" { dev.Manufacturer = pciids.VendorName(dev.VendorID) } @@ -1083,7 +1226,7 @@ func isMissingOrRawPCIModel(model string) bool { func isGenericPCIeClassLabel(v string) bool { switch strings.ToLower(strings.TrimSpace(v)) { - case "", "pcie device", "display", "display controller", "vga", "3d controller", "network", "network controller", "storage", "storage controller", "other", "unknown": + case "", "pcie device", "display", "display controller", "vga", "3d controller", "network", "network controller", "storage", "storage controller", "other", "unknown", "singlefunction", "multifunction", "simulated": return true default: return strings.HasPrefix(strings.ToLower(strings.TrimSpace(v)), "0x") diff --git a/internal/collector/redfish_replay.go b/internal/collector/redfish_replay.go index 1bf3eae..8fe3d78 100644 --- a/internal/collector/redfish_replay.go +++ b/internal/collector/redfish_replay.go @@ -263,9 +263,32 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag out = append(out, parseDrive(driveDoc)) } } + for _, chassisPath := range chassisPaths { + if !isSupermicroNVMeBackplanePath(chassisPath) { + continue + } + for _, driveDoc := range r.probeSupermicroNVMeDiskBays(chassisPath) { + if !looksLikeDrive(driveDoc) { + continue + } + out = append(out, parseDrive(driveDoc)) + } + } return dedupeStorage(out) } +func (r redfishSnapshotReader) probeSupermicroNVMeDiskBays(backplanePath string) []map[string]interface{} { + var out []map[string]interface{} + for _, path := range supermicroNVMeDiskBayCandidates(backplanePath) { + doc, err := r.getJSON(path) + if err != nil || !looksLikeDrive(doc) { + continue + } + out = append(out, doc) + } + return out +} + func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.NetworkAdapter { var nics []models.NetworkAdapter seen := make(map[string]struct{}) @@ -276,6 +299,14 @@ func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.Netwo } for _, doc := range adapterDocs { nic := parseNIC(doc) + for _, pciePath := range networkAdapterPCIeDevicePaths(doc) { + pcieDoc, err := r.getJSON(pciePath) + if err != nil { + continue + } + functionDocs := r.getLinkedPCIeFunctions(pcieDoc) + enrichNICFromPCIe(&nic, pcieDoc, functionDocs) + } key := firstNonEmpty(nic.SerialNumber, nic.Slot+"|"+nic.Model) if key == "" { continue diff --git a/internal/collector/redfish_test.go b/internal/collector/redfish_test.go index 8f24b61..7d526ce 100644 --- a/internal/collector/redfish_test.go +++ b/internal/collector/redfish_test.go @@ -238,3 +238,124 @@ func TestParsePCIeDeviceSlot_EmptyMapFallsBackToID(t *testing.T) { t.Fatalf("slot should not stringify empty map") } } + +func TestEnrichNICFromPCIeFunctions(t *testing.T) { + nic := parseNIC(map[string]interface{}{ + "Id": "1", + "Model": "MCX75310AAS-NEAT", + "Manufacturer": "Supermicro", + "SerialNumber": "NIC-SN-1", + "Controllers": []interface{}{ + map[string]interface{}{ + "Links": map[string]interface{}{ + "PCIeDevices": []interface{}{ + map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/NIC1"}, + }, + }, + "Location": map[string]interface{}{ + "PartLocation": map[string]interface{}{"ServiceLabel": "PCIe Slot 1 (1)"}, + }, + }, + }, + }) + + pcieDoc := map[string]interface{}{ + "Id": "NIC1", + "PCIeFunctions": map[string]interface{}{ + "@odata.id": "/redfish/v1/Chassis/1/PCIeDevices/NIC1/PCIeFunctions", + }, + } + functionDocs := []map[string]interface{}{ + { + "VendorId": "0x15b3", + "DeviceId": "0x1021", + }, + } + + enrichNICFromPCIe(&nic, pcieDoc, functionDocs) + if nic.VendorID != 0x15b3 || nic.DeviceID != 0x1021 { + t.Fatalf("unexpected NIC IDs: vendor=%#x device=%#x", nic.VendorID, nic.DeviceID) + } + if nic.Location != "PCIe Slot 1 (1)" { + t.Fatalf("unexpected NIC location: %q", nic.Location) + } +} + +func TestParsePCIeDevice_PrefersFunctionClassOverDeviceType(t *testing.T) { + doc := map[string]interface{}{ + "Id": "NIC1", + "DeviceType": "SingleFunction", + "Model": "MCX75310AAS-NEAT", + "PartNumber": "MCX75310AAS-NEAT", + } + functionDocs := []map[string]interface{}{ + { + "DeviceClass": "NetworkController", + "VendorId": "0x15b3", + "DeviceId": "0x1021", + }, + } + + got := parsePCIeDevice(doc, functionDocs) + if got.DeviceClass == "SingleFunction" { + t.Fatalf("device class should not keep generic redfish DeviceType") + } + if got.DeviceClass == "" { + t.Fatalf("device class should be resolved") + } +} + +func TestReplayCollectStorage_ProbesSupermicroNVMeDiskBayWhenCollectionEmpty(t *testing.T) { + r := redfishSnapshotReader{tree: map[string]interface{}{ + "/redfish/v1/Systems": map[string]interface{}{ + "Members": []interface{}{ + map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1"}, + }, + }, + "/redfish/v1/Systems/1/Storage": map[string]interface{}{ + "Members": []interface{}{ + map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/NVMeSSD"}, + }, + }, + "/redfish/v1/Systems/1/Storage/NVMeSSD": map[string]interface{}{ + "Drives": map[string]interface{}{"@odata.id": "/redfish/v1/Systems/1/Storage/NVMeSSD/Drives"}, + }, + "/redfish/v1/Systems/1/Storage/NVMeSSD/Drives": map[string]interface{}{ + "Members": []interface{}{}, + }, + "/redfish/v1/Chassis": map[string]interface{}{ + "Members": []interface{}{ + map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane"}, + }, + }, + "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane": map[string]interface{}{ + "Drives": map[string]interface{}{"@odata.id": "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives"}, + }, + "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives": map[string]interface{}{ + "Members@odata.count": 0, + "Members": []interface{}{}, + }, + "/redfish/v1/Chassis/NVMeSSD.0.Group.0.StorageBackplane/Drives/Disk.Bay.0": map[string]interface{}{ + "Id": "Disk.Bay.0", + "Name": "Disk.Bay.0", + "Manufacturer": "INTEL", + "SerialNumber": "BTLJ035203XT1P0FGN", + "Model": "INTEL SSDPE2KX010T8", + "CapacityBytes": int64(1000204886016), + "Protocol": "NVMe", + "MediaType": "SSD", + "Status": map[string]interface{}{"State": "Enabled", "Health": "OK"}, + }, + }} + + got := r.collectStorage("/redfish/v1/Systems/1") + if len(got) != 1 { + t.Fatalf("expected one drive from direct Disk.Bay probe, got %d", len(got)) + } + if got[0].SerialNumber != "BTLJ035203XT1P0FGN" { + t.Fatalf("unexpected serial: %q", got[0].SerialNumber) + } + if got[0].SizeGB == 0 { + t.Fatalf("expected size to be parsed from CapacityBytes") + } +}