raid: parse storcli64/storcli2's actual per-slot drive JSON shape
RAID Controller Management showed "No drives detected" for a live
SAS3808-iMR controller even though storcli64 clearly enumerates its
drives. Root cause: the "eall/sall show all J" drive-listing parser
(collector/raid.go and its webui/raid_mgmt.go duplicate) assumed
drives are reported as a single "Drive Information" array, but real
storcli64/storcli2 output nests each drive under its own dynamically
named key ("Drive /c0/e69/s0", paired with a "... - Detailed
Information" key) — confirmed against a live techdump/storcli64-drives.json
capture. The assumed shape was never actually produced by the tool, so
this affected every storcli64/storcli2 controller, not just Tri-Mode
ones (the storcli2 fallback added in b7f015c never got a chance to
mask it in practice, since storcli2 itself reports zero controllers on
this hardware — a separate, unrelated tool-side gap).
Both parsers now read "Response Data" as a raw key map and pull drives
from either shape, so older storcli output using the array form still
works alongside the real per-slot form.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2d84ddb577
commit
4e306ff78c
@@ -103,13 +103,16 @@ func collectStorcliDrives() []schema.HardwareStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// collectStorcli2Drives covers Broadcom Tri-Mode controllers (e.g.
|
// collectStorcli2Drives covers Broadcom Tri-Mode controllers (e.g.
|
||||||
// SAS3808-iMR/9500 series), which storcli64 can enumerate at a basic level
|
// SAS3808-iMR/9500 series) via storcli2, Broadcom's separate tool for this
|
||||||
// but whose drives it never finds — a separate tool/JSON schema (storcli2)
|
// controller line. On at least one confirmed SAS3808-iMR system storcli2
|
||||||
// is required. storcli2's "Drive Information" array shape matches storcli64's
|
// itself reports zero controllers even though storcli64 sees the controller
|
||||||
// (confirmed against Broadcom's published drive-info-schema.json), so
|
// and its drives fine (a tool-side gap, not something parseable around), so
|
||||||
// parseStorcliDrivesJSON is reused as-is; only the controller enumeration
|
// this is run as an additional source alongside storcli64, not a
|
||||||
// and per-controller invocation syntax differ (storcli2 wants an explicit
|
// replacement. parseStorcliDrivesJSON is reused as-is since storcli2 and
|
||||||
// /cN target rather than storcli64's /call wildcard).
|
// storcli64 share the same "Response Data" drive-listing shape; only the
|
||||||
|
// controller enumeration and per-controller invocation syntax differ
|
||||||
|
// (storcli2 wants an explicit /cN target rather than storcli64's /call
|
||||||
|
// wildcard).
|
||||||
func collectStorcli2Drives() []schema.HardwareStorage {
|
func collectStorcli2Drives() []schema.HardwareStorage {
|
||||||
sysOut, err := raidToolQuery("storcli2", "show", "all", "J")
|
sysOut, err := raidToolQuery("storcli2", "show", "all", "J")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -492,11 +495,7 @@ func parseSSACLIInterface(raw string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseStorcliDrivesJSON(raw []byte) []schema.HardwareStorage {
|
type storcliDrive struct {
|
||||||
var doc struct {
|
|
||||||
Controllers []struct {
|
|
||||||
ResponseData struct {
|
|
||||||
DriveInformation []struct {
|
|
||||||
EIDSlt string `json:"EID:Slt"`
|
EIDSlt string `json:"EID:Slt"`
|
||||||
State string `json:"State"`
|
State string `json:"State"`
|
||||||
Size string `json:"Size"`
|
Size string `json:"Size"`
|
||||||
@@ -506,8 +505,20 @@ func parseStorcliDrivesJSON(raw []byte) []schema.HardwareStorage {
|
|||||||
SN string `json:"SN"`
|
SN string `json:"SN"`
|
||||||
Sp string `json:"Sp"`
|
Sp string `json:"Sp"`
|
||||||
Type string `json:"Type"`
|
Type string `json:"Type"`
|
||||||
} `json:"Drive Information"`
|
}
|
||||||
} `json:"Response Data"`
|
|
||||||
|
// storcliDrivePerSlotKeyRe matches "Response Data" keys like "Drive /c0/e69/s0"
|
||||||
|
// — real storcli64/storcli2 "eall/sall show all J" output nests each drive
|
||||||
|
// under its own dynamically-named key instead of a shared "Drive Information"
|
||||||
|
// array (confirmed against a live SAS3808-iMR dump: the top-level "Drive
|
||||||
|
// Information" array this parser previously assumed simply isn't present).
|
||||||
|
// It must not match the paired "Drive /c0/e69/s0 - Detailed Information" key.
|
||||||
|
var storcliDrivePerSlotKeyRe = regexp.MustCompile(`^Drive /c\d+/e\d+/s\d+$`)
|
||||||
|
|
||||||
|
func parseStorcliDrivesJSON(raw []byte) []schema.HardwareStorage {
|
||||||
|
var doc struct {
|
||||||
|
Controllers []struct {
|
||||||
|
ResponseData map[string]json.RawMessage `json:"Response Data"`
|
||||||
} `json:"Controllers"`
|
} `json:"Controllers"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||||
@@ -517,26 +528,37 @@ func parseStorcliDrivesJSON(raw []byte) []schema.HardwareStorage {
|
|||||||
|
|
||||||
var drives []schema.HardwareStorage
|
var drives []schema.HardwareStorage
|
||||||
for _, ctl := range doc.Controllers {
|
for _, ctl := range doc.Controllers {
|
||||||
for _, d := range ctl.ResponseData.DriveInformation {
|
// Older storcli output (or "show" without "all") reports a single
|
||||||
|
// "Drive Information" array instead of per-slot keys.
|
||||||
|
if raw, ok := ctl.ResponseData["Drive Information"]; ok {
|
||||||
|
var list []storcliDrive
|
||||||
|
if err := json.Unmarshal(raw, &list); err == nil {
|
||||||
|
for _, d := range list {
|
||||||
if s := storcliDriveToStorage(d); s != nil {
|
if s := storcliDriveToStorage(d); s != nil {
|
||||||
drives = append(drives, *s)
|
drives = append(drives, *s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
for key, raw := range ctl.ResponseData {
|
||||||
|
if !storcliDrivePerSlotKeyRe.MatchString(key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var list []storcliDrive
|
||||||
|
if err := json.Unmarshal(raw, &list); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, d := range list {
|
||||||
|
if s := storcliDriveToStorage(d); s != nil {
|
||||||
|
drives = append(drives, *s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return drives
|
return drives
|
||||||
}
|
}
|
||||||
|
|
||||||
func storcliDriveToStorage(d struct {
|
func storcliDriveToStorage(d storcliDrive) *schema.HardwareStorage {
|
||||||
EIDSlt string `json:"EID:Slt"`
|
|
||||||
State string `json:"State"`
|
|
||||||
Size string `json:"Size"`
|
|
||||||
Intf string `json:"Intf"`
|
|
||||||
Med string `json:"Med"`
|
|
||||||
Model string `json:"Model"`
|
|
||||||
SN string `json:"SN"`
|
|
||||||
Sp string `json:"Sp"`
|
|
||||||
Type string `json:"Type"`
|
|
||||||
}) *schema.HardwareStorage {
|
|
||||||
present := true
|
present := true
|
||||||
status := mapRAIDDriveStatus(d.State)
|
status := mapRAIDDriveStatus(d.State)
|
||||||
s := schema.HardwareStorage{
|
s := schema.HardwareStorage{
|
||||||
|
|||||||
@@ -73,3 +73,38 @@ func TestCollectStorcli2DrivesEndToEnd(t *testing.T) {
|
|||||||
t.Fatalf("drives[0]=%#v want serial S6EYNE0T123456", drives[0])
|
t.Fatalf("drives[0]=%#v want serial S6EYNE0T123456", drives[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseStorcliDrivesJSONPerSlotKeys regression-tests against a live
|
||||||
|
// SAS3808-iMR "storcli64 /call/eall/sall show all J" dump: "Response Data"
|
||||||
|
// has no "Drive Information" array at all, only per-slot keys like
|
||||||
|
// "Drive /c0/e69/s0" (plus a paired "... - Detailed Information" key that
|
||||||
|
// must be skipped). The previous parser, which only looked for "Drive
|
||||||
|
// Information", silently returned zero drives against this real output.
|
||||||
|
func TestParseStorcliDrivesJSONPerSlotKeys(t *testing.T) {
|
||||||
|
raw := []byte(`{
|
||||||
|
"Controllers": [
|
||||||
|
{
|
||||||
|
"Command Status": {"Status": "Success"},
|
||||||
|
"Response Data": {
|
||||||
|
"Drive /c0/e69/s0": [
|
||||||
|
{"EID:Slt": "69:0", "DID": 0, "State": "JBOD", "DG": "-", "Size": "447.130 GB", "Intf": "SATA", "Med": "SSD", "Model": "SAMSUNG MZ7L3480HCHQ-00B7C", "Sp": "U", "Type": "-"}
|
||||||
|
],
|
||||||
|
"Drive /c0/e69/s0 - Detailed Information": {
|
||||||
|
"Drive /c0/e69/s0 State": {"Shield Counter": 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
drives := parseStorcliDrivesJSON(raw)
|
||||||
|
if len(drives) != 1 {
|
||||||
|
t.Fatalf("drives=%d want 1 (%#v)", len(drives), drives)
|
||||||
|
}
|
||||||
|
if drives[0].Model == nil || *drives[0].Model != "SAMSUNG MZ7L3480HCHQ-00B7C" {
|
||||||
|
t.Fatalf("drives[0]=%#v want model SAMSUNG MZ7L3480HCHQ-00B7C", drives[0])
|
||||||
|
}
|
||||||
|
if drives[0].Slot == nil || *drives[0].Slot != "69:0" {
|
||||||
|
t.Fatalf("drives[0].Slot=%v want 69:0", drives[0].Slot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,17 +72,7 @@ func detectLSIControllers() []raidControllerInfo {
|
|||||||
|
|
||||||
var driveDoc struct {
|
var driveDoc struct {
|
||||||
Controllers []struct {
|
Controllers []struct {
|
||||||
ResponseData struct {
|
ResponseData map[string]json.RawMessage `json:"Response Data"`
|
||||||
DriveInformation []struct {
|
|
||||||
EIDSlt string `json:"EID:Slt"`
|
|
||||||
State string `json:"State"`
|
|
||||||
Size string `json:"Size"`
|
|
||||||
Intf string `json:"Intf"`
|
|
||||||
Med string `json:"Med"`
|
|
||||||
Model string `json:"Model"`
|
|
||||||
SN string `json:"SN"`
|
|
||||||
} `json:"Drive Information"`
|
|
||||||
} `json:"Response Data"`
|
|
||||||
} `json:"Controllers"`
|
} `json:"Controllers"`
|
||||||
}
|
}
|
||||||
if len(driveOut) > 0 {
|
if len(driveOut) > 0 {
|
||||||
@@ -105,22 +95,8 @@ func detectLSIControllers() []raidControllerInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if i < len(driveDoc.Controllers) {
|
if i < len(driveDoc.Controllers) {
|
||||||
for _, d := range driveDoc.Controllers[i].ResponseData.DriveInformation {
|
ctrl.AllDrives, ctrl.ForeignDrives, ctrl.FreeDrives = classifyStorcliDrives(
|
||||||
info := raidDriveInfo{
|
parseStorcliResponseDataDrives(driveDoc.Controllers[i].ResponseData))
|
||||||
Slot: strings.TrimSpace(d.EIDSlt),
|
|
||||||
Model: strings.TrimSpace(d.Model),
|
|
||||||
State: strings.TrimSpace(d.State),
|
|
||||||
SizeGB: raidParseHumanSizeGB(d.Size),
|
|
||||||
Serial: strings.TrimSpace(d.SN),
|
|
||||||
}
|
|
||||||
ctrl.AllDrives = append(ctrl.AllDrives, info)
|
|
||||||
switch strings.TrimSpace(d.State) {
|
|
||||||
case "Frgn":
|
|
||||||
ctrl.ForeignDrives = append(ctrl.ForeignDrives, info)
|
|
||||||
case "UGood", "JBOD":
|
|
||||||
ctrl.FreeDrives = append(ctrl.FreeDrives, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controllers = append(controllers, ctrl)
|
controllers = append(controllers, ctrl)
|
||||||
@@ -128,15 +104,78 @@ func detectLSIControllers() []raidControllerInfo {
|
|||||||
return controllers
|
return controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// storcliDriveJSON is the per-drive record shape storcli64/storcli2 emit,
|
||||||
|
// whether nested in a "Drive Information" array or a per-slot "Drive
|
||||||
|
// /cX/eY/sZ" key (see parseStorcliResponseDataDrives).
|
||||||
|
type storcliDriveJSON struct {
|
||||||
|
EIDSlt string `json:"EID:Slt"`
|
||||||
|
State string `json:"State"`
|
||||||
|
Size string `json:"Size"`
|
||||||
|
Model string `json:"Model"`
|
||||||
|
SN string `json:"SN"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// storcliDrivePerSlotKeyRe matches "Response Data" keys like "Drive /c0/e69/s0".
|
||||||
|
// Real storcli64/storcli2 "eall/sall show all J" output nests each drive under
|
||||||
|
// its own dynamically-named key instead of a shared "Drive Information" array
|
||||||
|
// — confirmed against a live SAS3808-iMR dump that has no "Drive Information"
|
||||||
|
// key at all. Must not match the paired "... - Detailed Information" key.
|
||||||
|
var storcliDrivePerSlotKeyRe = regexp.MustCompile(`^Drive /c\d+/e\d+/s\d+$`)
|
||||||
|
|
||||||
|
// parseStorcliResponseDataDrives extracts drive records from one controller's
|
||||||
|
// "Response Data" object, supporting both the legacy "Drive Information"
|
||||||
|
// array shape and the per-slot dynamic-key shape.
|
||||||
|
func parseStorcliResponseDataDrives(responseData map[string]json.RawMessage) []storcliDriveJSON {
|
||||||
|
var drives []storcliDriveJSON
|
||||||
|
appendList := func(raw json.RawMessage) {
|
||||||
|
var list []storcliDriveJSON
|
||||||
|
if err := json.Unmarshal(raw, &list); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
drives = append(drives, list...)
|
||||||
|
}
|
||||||
|
if raw, ok := responseData["Drive Information"]; ok {
|
||||||
|
appendList(raw)
|
||||||
|
}
|
||||||
|
for key, raw := range responseData {
|
||||||
|
if storcliDrivePerSlotKeyRe.MatchString(key) {
|
||||||
|
appendList(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return drives
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifyStorcliDrives(drives []storcliDriveJSON) (all, foreign, free []raidDriveInfo) {
|
||||||
|
for _, d := range drives {
|
||||||
|
info := raidDriveInfo{
|
||||||
|
Slot: strings.TrimSpace(d.EIDSlt),
|
||||||
|
Model: strings.TrimSpace(d.Model),
|
||||||
|
State: strings.TrimSpace(d.State),
|
||||||
|
SizeGB: raidParseHumanSizeGB(d.Size),
|
||||||
|
Serial: strings.TrimSpace(d.SN),
|
||||||
|
}
|
||||||
|
all = append(all, info)
|
||||||
|
switch strings.TrimSpace(d.State) {
|
||||||
|
case "Frgn":
|
||||||
|
foreign = append(foreign, info)
|
||||||
|
case "UGood", "JBOD":
|
||||||
|
free = append(free, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all, foreign, free
|
||||||
|
}
|
||||||
|
|
||||||
// --- LSI/storcli2 detection (Tri-Mode controllers, e.g. SAS3808-iMR/9500) ---
|
// --- LSI/storcli2 detection (Tri-Mode controllers, e.g. SAS3808-iMR/9500) ---
|
||||||
//
|
//
|
||||||
// storcli2 is a separate tool/JSON schema from storcli64, required for
|
// storcli2 is Broadcom's separate tool/JSON schema for the Tri-Mode MegaRAID
|
||||||
// Broadcom's Tri-Mode MegaRAID line. storcli64 can still enumerate a Tri-Mode
|
// line; on hardware where it works, storcli64 can still enumerate the same
|
||||||
// controller at a basic level (hence it shows up once via detectLSIControllers)
|
// controller at a basic level (hence it may also show up via
|
||||||
// but its drive-listing JSON parser finds no "Drive Information" for these
|
// detectLSIControllers), so this is run as an additional source alongside
|
||||||
// controllers, silently reporting zero drives — this is the storcli2 path
|
// storcli64/VROC rather than a replacement. On at least one confirmed
|
||||||
// that actually understands them, run as an additional source alongside
|
// SAS3808-iMR system, storcli2 itself reports zero controllers even though
|
||||||
// storcli64/VROC rather than a replacement.
|
// storcli64 sees the controller and its drives fine — a separate,
|
||||||
|
// tool-side gap this webui can't work around beyond reporting nothing here
|
||||||
|
// and relying on detectLSIControllers.
|
||||||
func detectStorcli2Controllers() []raidControllerInfo {
|
func detectStorcli2Controllers() []raidControllerInfo {
|
||||||
sysOut, err := exec.Command("storcli2", "show", "all", "J").Output()
|
sysOut, err := exec.Command("storcli2", "show", "all", "J").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,49 +221,22 @@ func detectStorcli2Controllers() []raidControllerInfo {
|
|||||||
return controllers
|
return controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseStorcli2DriveInformation parses a single controller's
|
// parseStorcli2DriveInformation parses a single controller's "storcli2
|
||||||
// "storcli2 /cX/eall/sall show all J" output. The "Drive Information" array
|
// /cX/eall/sall show all J" output, which uses the same "Response Data"
|
||||||
// shape matches storcli64's schema (confirmed against Broadcom's published
|
// shape as storcli64 (see parseStorcliResponseDataDrives).
|
||||||
// drive-info-schema.json), so the same field set applies.
|
|
||||||
func parseStorcli2DriveInformation(raw []byte) (all, foreign, free []raidDriveInfo) {
|
func parseStorcli2DriveInformation(raw []byte) (all, foreign, free []raidDriveInfo) {
|
||||||
if len(raw) == 0 {
|
if len(raw) == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
var doc struct {
|
var doc struct {
|
||||||
Controllers []struct {
|
Controllers []struct {
|
||||||
ResponseData struct {
|
ResponseData map[string]json.RawMessage `json:"Response Data"`
|
||||||
DriveInformation []struct {
|
|
||||||
EIDSlt string `json:"EID:Slt"`
|
|
||||||
State string `json:"State"`
|
|
||||||
Size string `json:"Size"`
|
|
||||||
Intf string `json:"Intf"`
|
|
||||||
Med string `json:"Med"`
|
|
||||||
Model string `json:"Model"`
|
|
||||||
SN string `json:"SN"`
|
|
||||||
} `json:"Drive Information"`
|
|
||||||
} `json:"Response Data"`
|
|
||||||
} `json:"Controllers"`
|
} `json:"Controllers"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(raw, &doc); err != nil || len(doc.Controllers) == 0 {
|
if err := json.Unmarshal(raw, &doc); err != nil || len(doc.Controllers) == 0 {
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
for _, d := range doc.Controllers[0].ResponseData.DriveInformation {
|
return classifyStorcliDrives(parseStorcliResponseDataDrives(doc.Controllers[0].ResponseData))
|
||||||
info := raidDriveInfo{
|
|
||||||
Slot: strings.TrimSpace(d.EIDSlt),
|
|
||||||
Model: strings.TrimSpace(d.Model),
|
|
||||||
State: strings.TrimSpace(d.State),
|
|
||||||
SizeGB: raidParseHumanSizeGB(d.Size),
|
|
||||||
Serial: strings.TrimSpace(d.SN),
|
|
||||||
}
|
|
||||||
all = append(all, info)
|
|
||||||
switch strings.TrimSpace(d.State) {
|
|
||||||
case "Frgn":
|
|
||||||
foreign = append(foreign, info)
|
|
||||||
case "UGood", "JBOD":
|
|
||||||
free = append(free, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return all, foreign, free
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- VROC/mdadm detection ---
|
// --- VROC/mdadm detection ---
|
||||||
|
|||||||
@@ -47,3 +47,38 @@ func TestParseStorcli2DriveInformationEmptyOrMalformed(t *testing.T) {
|
|||||||
t.Fatalf("empty Controllers should return nil, got %#v", all)
|
t.Fatalf("empty Controllers should return nil, got %#v", all)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseStorcliResponseDataDrivesPerSlotKeys regression-tests against a
|
||||||
|
// live SAS3808-iMR "storcli64 /call/eall/sall show all J" dump: "Response
|
||||||
|
// Data" has no "Drive Information" array at all, only per-slot keys like
|
||||||
|
// "Drive /c0/e69/s0" (plus a paired "... - Detailed Information" key that
|
||||||
|
// must be skipped). This is what made "RAID Controller Management" show
|
||||||
|
// "No drives detected" for a controller storcli64 could clearly see.
|
||||||
|
func TestParseStorcliResponseDataDrivesPerSlotKeys(t *testing.T) {
|
||||||
|
raw := []byte(`{
|
||||||
|
"Controllers": [
|
||||||
|
{
|
||||||
|
"Command Status": {"Status": "Success"},
|
||||||
|
"Response Data": {
|
||||||
|
"Drive /c0/e69/s0": [
|
||||||
|
{"EID:Slt": "69:0", "State": "JBOD", "Size": "447.130 GB", "Model": "SAMSUNG MZ7L3480HCHQ-00B7C"}
|
||||||
|
],
|
||||||
|
"Drive /c0/e69/s0 - Detailed Information": {
|
||||||
|
"Drive /c0/e69/s0 State": {"Shield Counter": 0}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
all, _, free := parseStorcli2DriveInformation(raw)
|
||||||
|
if len(all) != 1 {
|
||||||
|
t.Fatalf("all=%d want 1 (%#v)", len(all), all)
|
||||||
|
}
|
||||||
|
if all[0].Model != "SAMSUNG MZ7L3480HCHQ-00B7C" || all[0].Slot != "69:0" {
|
||||||
|
t.Fatalf("all[0]=%#v want model/slot populated", all[0])
|
||||||
|
}
|
||||||
|
if len(free) != 1 {
|
||||||
|
t.Fatalf("free=%d want 1 (JBOD is a free drive)", len(free))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user