Probe Supermicro NVMe Disk.Bay endpoints for drive inventory
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user