package webui import ( "encoding/json" "fmt" "os" "os/exec" "regexp" "strings" ) // --- Response types --- type raidDriveInfo struct { Slot string `json:"slot,omitempty"` Device string `json:"device,omitempty"` Model string `json:"model,omitempty"` SizeGB float64 `json:"size_gb,omitempty"` Serial string `json:"serial,omitempty"` State string `json:"state,omitempty"` } type raidArrayInfo struct { Name string `json:"name"` Level string `json:"level,omitempty"` Members []string `json:"members"` Degraded bool `json:"degraded"` } type raidControllerInfo struct { ID string `json:"id"` Type string `json:"type"` Index int `json:"index"` Model string `json:"model"` ForeignDrives []raidDriveInfo `json:"foreign_drives"` FreeDrives []raidDriveInfo `json:"free_drives"` AllDrives []raidDriveInfo `json:"all_drives"` Arrays []raidArrayInfo `json:"arrays,omitempty"` } type raidStatusResp struct { Controllers []raidControllerInfo `json:"controllers"` } // --- LSI/storcli detection --- func detectLSIControllers() []raidControllerInfo { ctrlOut, err := exec.Command("storcli64", "/call", "show", "J").Output() if err != nil { return nil } var ctrlDoc struct { Controllers []struct { ResponseData struct { Basics struct { Controller int `json:"Controller"` Model string `json:"Model"` } `json:"Basics"` } `json:"Response Data"` } `json:"Controllers"` } if err := json.Unmarshal(ctrlOut, &ctrlDoc); err != nil || len(ctrlDoc.Controllers) == 0 { return nil } driveOut, _ := exec.Command("storcli64", "/call/eall/sall", "show", "all", "J").Output() var driveDoc struct { Controllers []struct { ResponseData map[string]json.RawMessage `json:"Response Data"` } `json:"Controllers"` } if len(driveOut) > 0 { json.Unmarshal(driveOut, &driveDoc) //nolint:errcheck } var controllers []raidControllerInfo for i, c := range ctrlDoc.Controllers { ctrl := raidControllerInfo{ ID: fmt.Sprintf("lsi-%d", c.ResponseData.Basics.Controller), Type: "lsi", Index: c.ResponseData.Basics.Controller, Model: c.ResponseData.Basics.Model, ForeignDrives: []raidDriveInfo{}, FreeDrives: []raidDriveInfo{}, AllDrives: []raidDriveInfo{}, } if ctrl.Model == "" { ctrl.Model = fmt.Sprintf("LSI Controller %d", ctrl.Index) } if i < len(driveDoc.Controllers) { ctrl.AllDrives, ctrl.ForeignDrives, ctrl.FreeDrives = classifyStorcliDrives( parseStorcliResponseDataDrives(driveDoc.Controllers[i].ResponseData)) } controllers = append(controllers, ctrl) } 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) --- // // storcli2 is Broadcom's separate tool/JSON schema for the Tri-Mode MegaRAID // line; on hardware where it works, storcli64 can still enumerate the same // controller at a basic level (hence it may also show up via // detectLSIControllers), so this is run as an additional source alongside // storcli64/VROC rather than a replacement. On at least one confirmed // SAS3808-iMR system, storcli2 itself reports zero controllers even though // 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 { 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, which uses the same "Response Data" // shape as storcli64 (see parseStorcliResponseDataDrives). func parseStorcli2DriveInformation(raw []byte) (all, foreign, free []raidDriveInfo) { if len(raw) == 0 { return nil, nil, nil } var doc struct { Controllers []struct { ResponseData map[string]json.RawMessage `json:"Response Data"` } `json:"Controllers"` } if err := json.Unmarshal(raw, &doc); err != nil || len(doc.Controllers) == 0 { return nil, nil, nil } return classifyStorcliDrives(parseStorcliResponseDataDrives(doc.Controllers[0].ResponseData)) } // --- VROC/mdadm detection --- var raidMDStatDegradedRx = regexp.MustCompile(`\[[U_]+\]`) type mdStatEntry struct { Name string Level string Members []string Degraded bool } func parseRAIDMDStat(raw string) []mdStatEntry { var entries []mdStatEntry var cur *mdStatEntry for _, line := range strings.Split(raw, "\n") { if strings.HasPrefix(line, "Personalities") || strings.HasPrefix(line, "unused devices") { continue } if idx := strings.Index(line, " : "); idx > 0 { name := strings.TrimSpace(line[:idx]) rest := line[idx+3:] entry := mdStatEntry{Name: name} for _, tok := range strings.Fields(rest) { if strings.HasPrefix(tok, "raid") || strings.HasPrefix(tok, "linear") { entry.Level = tok } if bk := strings.Index(tok, "["); bk > 0 && strings.HasSuffix(tok, "]") { entry.Members = append(entry.Members, tok[:bk]) } } entries = append(entries, entry) cur = &entries[len(entries)-1] continue } if cur != nil { if m := raidMDStatDegradedRx.FindString(line); m != "" && strings.Contains(m, "_") { cur.Degraded = true } } } return entries } // raidVROCPortRx matches lines like " Port2 : /dev/sda (SERIAL123)" // or " Port3 : - no device attached -" from `mdadm --detail-platform`. var raidVROCPortRx = regexp.MustCompile(`^\s*Port\d+\s*:\s*(\S+)`) // parseVROCPorts returns the block device basenames (e.g. "sda") that are // physically wired to the VROC I/O controller's ports, per `mdadm // --detail-platform` output. Drives attached directly to the CPU (or to a // separate HBA) rather than through this controller's ports are excluded. func parseVROCPorts(raw string) map[string]bool { ports := map[string]bool{} for _, line := range strings.Split(raw, "\n") { m := raidVROCPortRx.FindStringSubmatch(line) if m == nil { continue } dev := m[1] if !strings.HasPrefix(dev, "/dev/") { continue } ports[strings.TrimPrefix(dev, "/dev/")] = true } return ports } func detectVROCController() *raidControllerInfo { out, err := exec.Command("mdadm", "--detail-platform").CombinedOutput() if err != nil && len(out) == 0 { return nil } hasVROC := false for _, line := range strings.Split(string(out), "\n") { lower := strings.ToLower(line) if strings.Contains(lower, "license") || strings.Contains(lower, "intel") || strings.Contains(lower, "platform") { hasVROC = true break } } if !hasVROC { return nil } ctrl := &raidControllerInfo{ ID: "vroc-0", Type: "vroc", Model: "Intel VROC", ForeignDrives: []raidDriveInfo{}, FreeDrives: []raidDriveInfo{}, AllDrives: []raidDriveInfo{}, } ports := parseVROCPorts(string(out)) // Some mdadm builds omit the "Port" lines from --detail-platform. When // we can't determine which drives are actually wired to this // controller, fall back to showing every disk not already in an array // rather than hiding everything. portsKnown := len(ports) > 0 inArray := map[string]bool{} raw, err := os.ReadFile("/proc/mdstat") if err == nil { for _, arr := range parseRAIDMDStat(string(raw)) { ctrl.Arrays = append(ctrl.Arrays, raidArrayInfo{ Name: arr.Name, Level: arr.Level, Members: arr.Members, Degraded: arr.Degraded, }) for _, m := range arr.Members { inArray[m] = true } } } lsblkOut, err := exec.Command("lsblk", "-J", "-d", "-o", "NAME,SIZE,TYPE,MODEL,SERIAL").Output() if err == nil { var lsblkDoc struct { BlockDevices []struct { Name string `json:"name"` Size string `json:"size"` Type string `json:"type"` Model string `json:"model"` Serial string `json:"serial"` } `json:"blockdevices"` } if json.Unmarshal(lsblkOut, &lsblkDoc) == nil { for _, d := range lsblkDoc.BlockDevices { // Only consider disks wired to this controller's ports - // drives attached directly to the CPU (or another // controller) never show up as VROC ports and are skipped. if d.Type != "disk" || (portsKnown && !ports[d.Name]) { continue } info := raidDriveInfo{ Device: "/dev/" + d.Name, Model: strings.TrimSpace(d.Model), Serial: strings.TrimSpace(d.Serial), State: "available", } if inArray[d.Name] { info.State = "member" } ctrl.AllDrives = append(ctrl.AllDrives, info) if info.State == "available" { ctrl.FreeDrives = append(ctrl.FreeDrives, info) } } } } return ctrl } // --- API handlers ---