394 lines
12 KiB
Go
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 ---
|