Files

394 lines
12 KiB
Go

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 ---