package webui import ( "context" "encoding/json" "fmt" "net/http" "os" "os/exec" "regexp" "strconv" "strings" "time" ) // --- 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"` 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 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 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{}, } if ctrl.Model == "" { ctrl.Model = fmt.Sprintf("LSI Controller %d", ctrl.Index) } if i < len(driveDoc.Controllers) { for _, d := range driveDoc.Controllers[i].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), } 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) } return controllers } // --- 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 } 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{}, } 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 { if d.Type != "disk" || inArray[d.Name] { continue } ctrl.FreeDrives = append(ctrl.FreeDrives, raidDriveInfo{ Device: "/dev/" + d.Name, Model: strings.TrimSpace(d.Model), Serial: strings.TrimSpace(d.Serial), State: "available", }) } } } return ctrl } // --- API handlers --- func (h *handler) handleAPIRAIDStatus(w http.ResponseWriter, r *http.Request) { resp := raidStatusResp{Controllers: []raidControllerInfo{}} if lsi := detectLSIControllers(); len(lsi) > 0 { resp.Controllers = append(resp.Controllers, lsi...) } if vroc := detectVROCController(); vroc != nil { resp.Controllers = append(resp.Controllers, *vroc) } writeJSON(w, resp) } func (h *handler) handleAPIRAIDForeignAction(w http.ResponseWriter, r *http.Request) { var req struct { ControllerID string `json:"controller_id"` Action string `json:"action"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.Action != "import" && req.Action != "clear" { writeError(w, http.StatusBadRequest, "action must be 'import' or 'clear'") return } ctrlIdx, ok := parseLSIControllerIndex(req.ControllerID) if !ok { writeError(w, http.StatusBadRequest, "invalid controller_id") return } target := "raid-foreign-clear" name := fmt.Sprintf("RAID Foreign Clear (ctrl %d)", ctrlIdx) if req.Action == "import" { target = "raid-foreign-import" name = fmt.Sprintf("RAID Foreign Import (ctrl %d)", ctrlIdx) } t := &Task{ ID: newJobID(target), Name: name, Target: target, Priority: defaultTaskPriority(target, taskParams{}), Status: TaskPending, CreatedAt: time.Now(), params: taskParams{RAIDController: ctrlIdx}, } globalQueue.enqueue(t) writeJSON(w, map[string]string{"task_id": t.ID}) } func (h *handler) handleAPIRAIDCreateMirror(w http.ResponseWriter, r *http.Request) { var req struct { ControllerID string `json:"controller_id"` Devices []string `json:"devices"` ArrayName string `json:"array_name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if len(req.Devices) < 2 { writeError(w, http.StatusBadRequest, "at least 2 devices required") return } var target, name string var params taskParams switch { case strings.HasPrefix(req.ControllerID, "lsi-"): ctrlIdx, ok := parseLSIControllerIndex(req.ControllerID) if !ok { writeError(w, http.StatusBadRequest, "invalid controller_id") return } target = "raid-lsi-create-mirror" name = fmt.Sprintf("Create RAID 1 Mirror (LSI ctrl %d)", ctrlIdx) params = taskParams{RAIDController: ctrlIdx, RAIDDevices: req.Devices} case req.ControllerID == "vroc-0": arrayName := strings.TrimSpace(req.ArrayName) if arrayName == "" { arrayName = "bee-mirror0" } target = "raid-vroc-create-mirror" name = fmt.Sprintf("Create VROC RAID 1 (%s)", arrayName) params = taskParams{RAIDDevices: req.Devices, RAIDArrayName: arrayName} default: writeError(w, http.StatusBadRequest, "unknown controller_id") return } t := &Task{ ID: newJobID(target), Name: name, Target: target, Priority: defaultTaskPriority(target, taskParams{}), Status: TaskPending, CreatedAt: time.Now(), params: params, } globalQueue.enqueue(t) writeJSON(w, map[string]string{"task_id": t.ID}) } func parseLSIControllerIndex(id string) (int, bool) { if !strings.HasPrefix(id, "lsi-") { return 0, false } n, err := strconv.Atoi(strings.TrimPrefix(id, "lsi-")) if err != nil || n < 0 { return 0, false } return n, true } // --- Task runner functions --- func runRAIDForeignClearTask(ctx context.Context, j *jobState, ctrl int) error { j.append(fmt.Sprintf("Clearing foreign configuration on controller %d...", ctrl)) cmd := exec.CommandContext(ctx, "storcli64", fmt.Sprintf("/c%d/fall", ctrl), "del", "noprompt") return streamCmdJob(j, cmd) } func runRAIDForeignImportTask(ctx context.Context, j *jobState, ctrl int) error { j.append(fmt.Sprintf("Importing foreign configuration on controller %d...", ctrl)) cmd := exec.CommandContext(ctx, "storcli64", fmt.Sprintf("/c%d/fall", ctrl), "import", "noprompt") return streamCmdJob(j, cmd) } func runRAIDLSICreateMirrorTask(ctx context.Context, j *jobState, ctrl int, drives []string) error { driveList := strings.Join(drives, ",") j.append(fmt.Sprintf("Creating RAID 1 on controller %d with drives: %s", ctrl, driveList)) cmd := exec.CommandContext(ctx, "storcli64", fmt.Sprintf("/c%d", ctrl), "add", "vd", "type=raid1", fmt.Sprintf("drives=%s", driveList), "pdperarray=2", ) return streamCmdJob(j, cmd) } func runRAIDVROCCreateMirrorTask(ctx context.Context, j *jobState, devices []string, arrayName string) error { if arrayName == "" { arrayName = "bee-mirror0" } devPath := "/dev/md/" + arrayName args := []string{ "--create", devPath, "--level=1", fmt.Sprintf("--raid-devices=%d", len(devices)), "--run", } args = append(args, devices...) j.append(fmt.Sprintf("Creating VROC RAID 1 array %s with: %s", devPath, strings.Join(devices, " "))) cmd := exec.CommandContext(ctx, "mdadm", args...) return streamCmdJob(j, cmd) } // raidParseHumanSizeGB parses storcli size strings like "1.818 TB", "745.211 GB". func raidParseHumanSizeGB(s string) float64 { s = strings.TrimSpace(s) if s == "" { return 0 } upper := strings.ToUpper(s) var mul float64 var numStr string switch { case strings.Contains(upper, " TB"): mul = 1024 numStr = strings.TrimSpace(strings.SplitN(upper, " T", 2)[0]) case strings.Contains(upper, " GB"): mul = 1 numStr = strings.TrimSpace(strings.SplitN(upper, " G", 2)[0]) case strings.Contains(upper, " MB"): mul = 1.0 / 1024 numStr = strings.TrimSpace(strings.SplitN(upper, " M", 2)[0]) default: return 0 } v, err := strconv.ParseFloat(numStr, 64) if err != nil { return 0 } return v * mul } // --- UI card --- func renderRAIDMgmtCard() string { return `
RAID Controller Management
Loading...
` }