Probe Supermicro NVMe Disk.Bay endpoints for drive inventory

This commit is contained in:
Mikhail Chusavitin
2026-02-24 18:22:02 +03:00
parent 2e348751f3
commit a6c90b6e77
3 changed files with 299 additions and 4 deletions

View File

@@ -245,6 +245,17 @@ func (c *RedfishConnector) collectStorage(ctx context.Context, client *http.Clie
out = append(out, parseDrive(driveDoc)) 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) out = dedupeStorage(out)
return out return out
@@ -260,6 +271,14 @@ func (c *RedfishConnector) collectNICs(ctx context.Context, client *http.Client,
} }
for _, doc := range adapterDocs { for _, doc := range adapterDocs {
nic := parseNIC(doc) 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) key := firstNonEmpty(nic.SerialNumber, nic.Slot+"|"+nic.Model)
if key == "" { if key == "" {
continue continue
@@ -593,6 +612,25 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
close(stopHeartbeat) close(stopHeartbeat)
close(jobs) 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 { if emit != nil {
emit(Progress{ emit(Progress{
Status: "running", Status: "running",
@@ -604,6 +642,34 @@ func (c *RedfishConnector) collectRawRedfishTree(ctx context.Context, client *ht
return out 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 { func shouldCrawlPath(path string) bool {
if path == "" { if path == "" {
return false return false
@@ -845,10 +911,22 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
if strings.TrimSpace(vendor) == "" { if strings.TrimSpace(vendor) == "" {
vendor = pciids.VendorName(vendorID) 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{ return models.NetworkAdapter{
Slot: firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])), Slot: firstNonEmpty(asString(doc["Id"]), asString(doc["Name"])),
Location: asString(doc["Location"]), Location: location,
Present: !strings.EqualFold(mapStatus(doc["Status"]), "Absent"), Present: !strings.EqualFold(mapStatus(doc["Status"]), "Absent"),
Model: strings.TrimSpace(model), Model: strings.TrimSpace(model),
Vendor: strings.TrimSpace(vendor), Vendor: strings.TrimSpace(vendor),
@@ -856,10 +934,70 @@ func parseNIC(doc map[string]interface{}) models.NetworkAdapter {
DeviceID: deviceID, DeviceID: deviceID,
SerialNumber: asString(doc["SerialNumber"]), SerialNumber: asString(doc["SerialNumber"]),
PartNumber: asString(doc["PartNumber"]), PartNumber: asString(doc["PartNumber"]),
Firmware: firmware,
PortCount: portCount,
Status: mapStatus(doc["Status"]), 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 { func parsePSU(doc map[string]interface{}, idx int) models.PSU {
status := mapStatus(doc["Status"]) status := mapStatus(doc["Status"])
present := true present := true
@@ -969,7 +1107,7 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
dev := models.PCIeDevice{ dev := models.PCIeDevice{
Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), asString(doc["Name"]), asString(doc["Id"])), Slot: firstNonEmpty(redfishLocationLabel(doc["Slot"]), redfishLocationLabel(doc["Location"]), asString(doc["Name"]), asString(doc["Id"])),
BDF: asString(doc["BDF"]), BDF: asString(doc["BDF"]),
DeviceClass: firstNonEmpty(asString(doc["DeviceType"]), asString(doc["PCIeType"])), DeviceClass: asString(doc["DeviceType"]),
Manufacturer: asString(doc["Manufacturer"]), Manufacturer: asString(doc["Manufacturer"]),
PartNumber: asString(doc["PartNumber"]), PartNumber: asString(doc["PartNumber"]),
SerialNumber: asString(doc["SerialNumber"]), SerialNumber: asString(doc["SerialNumber"]),
@@ -981,7 +1119,7 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
if dev.BDF == "" { if dev.BDF == "" {
dev.BDF = asString(fn["FunctionId"]) dev.BDF = asString(fn["FunctionId"])
} }
if dev.DeviceClass == "" { if dev.DeviceClass == "" || isGenericPCIeClassLabel(dev.DeviceClass) {
dev.DeviceClass = firstNonEmpty(asString(fn["DeviceClass"]), asString(fn["ClassCode"])) dev.DeviceClass = firstNonEmpty(asString(fn["DeviceClass"]), asString(fn["ClassCode"]))
} }
if dev.VendorID == 0 { if dev.VendorID == 0 {
@@ -1012,6 +1150,11 @@ func parsePCIeDevice(doc map[string]interface{}, functionDocs []map[string]inter
dev.DeviceClass = resolved 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) == "" { if strings.TrimSpace(dev.Manufacturer) == "" {
dev.Manufacturer = pciids.VendorName(dev.VendorID) dev.Manufacturer = pciids.VendorName(dev.VendorID)
} }
@@ -1083,7 +1226,7 @@ func isMissingOrRawPCIModel(model string) bool {
func isGenericPCIeClassLabel(v string) bool { func isGenericPCIeClassLabel(v string) bool {
switch strings.ToLower(strings.TrimSpace(v)) { 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 return true
default: default:
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(v)), "0x") return strings.HasPrefix(strings.ToLower(strings.TrimSpace(v)), "0x")

View File

@@ -263,9 +263,32 @@ func (r redfishSnapshotReader) collectStorage(systemPath string) []models.Storag
out = append(out, parseDrive(driveDoc)) 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) 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 { func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.NetworkAdapter {
var nics []models.NetworkAdapter var nics []models.NetworkAdapter
seen := make(map[string]struct{}) seen := make(map[string]struct{})
@@ -276,6 +299,14 @@ func (r redfishSnapshotReader) collectNICs(chassisPaths []string) []models.Netwo
} }
for _, doc := range adapterDocs { for _, doc := range adapterDocs {
nic := parseNIC(doc) 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) key := firstNonEmpty(nic.SerialNumber, nic.Slot+"|"+nic.Model)
if key == "" { if key == "" {
continue continue

View File

@@ -238,3 +238,124 @@ func TestParsePCIeDeviceSlot_EmptyMapFallsBackToID(t *testing.T) {
t.Fatalf("slot should not stringify empty map") 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")
}
}