raid: add storcli2 support for Tri-Mode controllers (SAS3808-iMR/9500 series)

storcli64 enumerates these controllers but reports zero drives; storcli2
is the tool Broadcom ships for Tri-Mode/MegaRAID8 hardware and uses a
compatible JSON schema for drive listing. Wires storcli2 into both the
collector (structured drive data) and the webui RAID Management page
(dedup so a controller isn't double-listed if storcli64 already sees it
with zero drives), plus techdump raw collection and the ISO vendor-tool
build step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-07-07 15:25:06 +03:00
co-authored by Claude Sonnet 5
parent a3377083aa
commit b7f015c713
7 changed files with 307 additions and 2 deletions
+115 -1
View File
@@ -128,6 +128,105 @@ func detectLSIControllers() []raidControllerInfo {
return controllers
}
// --- LSI/storcli2 detection (Tri-Mode controllers, e.g. SAS3808-iMR/9500) ---
//
// storcli2 is a separate tool/JSON schema from storcli64, required for
// Broadcom's Tri-Mode MegaRAID line. storcli64 can still enumerate a Tri-Mode
// controller at a basic level (hence it shows up once via detectLSIControllers)
// but its drive-listing JSON parser finds no "Drive Information" for these
// controllers, silently reporting zero drives — this is the storcli2 path
// that actually understands them, run as an additional source alongside
// storcli64/VROC rather than a replacement.
func detectStorcli2Controllers() []raidControllerInfo {
sysOut, err := exec.Command("storcli2", "show", "all", "J").Output()
if err != nil {
return nil
}
var sysDoc struct {
Controllers []struct {
ResponseData struct {
SystemOverview []struct {
Ctrl int `json:"Ctrl"`
ProductName string `json:"Product Name"`
} `json:"System Overview"`
} `json:"Response Data"`
} `json:"Controllers"`
}
if err := json.Unmarshal(sysOut, &sysDoc); err != nil || len(sysDoc.Controllers) == 0 {
return nil
}
var controllers []raidControllerInfo
for _, entry := range sysDoc.Controllers {
for _, ov := range entry.ResponseData.SystemOverview {
ctrl := raidControllerInfo{
ID: fmt.Sprintf("lsi2-%d", ov.Ctrl),
Type: "lsi",
Index: ov.Ctrl,
Model: strings.TrimSpace(ov.ProductName),
ForeignDrives: []raidDriveInfo{},
FreeDrives: []raidDriveInfo{},
AllDrives: []raidDriveInfo{},
}
if ctrl.Model == "" {
ctrl.Model = fmt.Sprintf("LSI Controller %d", ctrl.Index)
}
driveOut, _ := exec.Command("storcli2", fmt.Sprintf("/c%d/eall/sall", ov.Ctrl), "show", "all", "J").Output()
ctrl.AllDrives, ctrl.ForeignDrives, ctrl.FreeDrives = parseStorcli2DriveInformation(driveOut)
controllers = append(controllers, ctrl)
}
}
return controllers
}
// parseStorcli2DriveInformation parses a single controller's
// "storcli2 /cX/eall/sall show all J" output. The "Drive Information" array
// shape matches storcli64's schema (confirmed against Broadcom's published
// drive-info-schema.json), so the same field set applies.
func parseStorcli2DriveInformation(raw []byte) (all, foreign, free []raidDriveInfo) {
if len(raw) == 0 {
return nil, nil, nil
}
var doc struct {
Controllers []struct {
ResponseData struct {
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"`
}
if err := json.Unmarshal(raw, &doc); err != nil || len(doc.Controllers) == 0 {
return nil, nil, nil
}
for _, d := range doc.Controllers[0].ResponseData.DriveInformation {
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 ---
var raidMDStatDegradedRx = regexp.MustCompile(`\[[U_]+\]`)
@@ -288,8 +387,23 @@ func detectVROCController() *raidControllerInfo {
func (h *handler) handleAPIRAIDStatus(w http.ResponseWriter, r *http.Request) {
resp := raidStatusResp{Controllers: []raidControllerInfo{}}
lsi2 := detectStorcli2Controllers()
if lsi := detectLSIControllers(); len(lsi) > 0 {
resp.Controllers = append(resp.Controllers, lsi...)
// storcli64 can enumerate a Tri-Mode controller (SAS3808-iMR/9500
// series) at a basic level but its drive-listing JSON parser finds
// no "Drive Information" for these — a zero-drives entry that
// storcli2 (run above) already covers correctly. Only drop it when
// storcli2 actually found something, so a genuinely drive-populated
// classic controller elsewhere in a mixed setup is never hidden.
for _, c := range lsi {
if len(c.AllDrives) == 0 && len(lsi2) > 0 {
continue
}
resp.Controllers = append(resp.Controllers, c)
}
}
if len(lsi2) > 0 {
resp.Controllers = append(resp.Controllers, lsi2...)
}
if vroc := detectVROCController(); vroc != nil {
resp.Controllers = append(resp.Controllers, *vroc)
+49
View File
@@ -0,0 +1,49 @@
package webui
import "testing"
// Fixture shape matches Broadcom's published storcli2 drive-info-schema.json
// (Controllers[].Response Data.Drive Information[]), which is the same shape
// storcli64 already uses — confirmed by comparing the two tools' schemas.
func TestParseStorcli2DriveInformation(t *testing.T) {
raw := []byte(`{
"Controllers": [
{
"Command Status": {"Controller": 0, "Status": "Success"},
"Response Data": {
"Drive Information": [
{"EID:Slt": "252:0", "State": "Onln", "Size": "1.746 TB", "Intf": "NVMe", "Med": "SSD", "Model": "SAMSUNG MZQL21T9HCJR", "SN": "S6EYNE0T123456"},
{"EID:Slt": "252:1", "State": "UGood", "Size": "1.746 TB", "Intf": "NVMe", "Med": "SSD", "Model": "SAMSUNG MZQL21T9HCJR", "SN": "S6EYNE0T654321"}
]
}
}
]
}`)
all, foreign, free := parseStorcli2DriveInformation(raw)
if len(all) != 2 {
t.Fatalf("all=%d want 2 (%#v)", len(all), all)
}
if len(foreign) != 0 {
t.Fatalf("foreign=%d want 0", len(foreign))
}
if len(free) != 1 || free[0].Serial != "S6EYNE0T654321" {
t.Fatalf("free=%#v want one UGood drive", free)
}
if all[0].Slot != "252:0" || all[0].SizeGB <= 0 {
t.Fatalf("all[0]=%#v want populated slot/size", all[0])
}
}
func TestParseStorcli2DriveInformationEmptyOrMalformed(t *testing.T) {
if all, foreign, free := parseStorcli2DriveInformation(nil); all != nil || foreign != nil || free != nil {
t.Fatalf("nil input should return nil slices, got %#v %#v %#v", all, foreign, free)
}
if all, _, _ := parseStorcli2DriveInformation([]byte("not json")); all != nil {
t.Fatalf("malformed JSON should return nil, got %#v", all)
}
// Valid JSON but no Controllers (e.g. tool ran but found nothing) must not panic.
if all, _, _ := parseStorcli2DriveInformation([]byte(`{"Controllers":[]}`)); all != nil {
t.Fatalf("empty Controllers should return nil, got %#v", all)
}
}