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
+147 -4
View File
@@ -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")