mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
260 lines
7 KiB
Go
260 lines
7 KiB
Go
package mdadm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
|
|
)
|
|
|
|
// Pre-compiled regexes for performance (avoid recompilation on each call)
|
|
var (
|
|
mdDeviceRe = regexp.MustCompile(`^(md\d+)\s*:`)
|
|
slotRe = regexp.MustCompile(`^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.+?)\s+(/dev/.+)$`)
|
|
speedRe = regexp.MustCompile(`speed=(\S+)`)
|
|
runCommandOutput = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
return cmd.Output()
|
|
}
|
|
)
|
|
|
|
// CollectArrays discovers and collects status for all mdadm RAID arrays on the system.
|
|
// Returns an empty slice if mdadm is not available or no arrays are found.
|
|
func CollectArrays(ctx context.Context) ([]host.RAIDArray, error) {
|
|
// Check if mdadm is available
|
|
if !isMdadmAvailable(ctx) {
|
|
return nil, nil
|
|
}
|
|
|
|
// Get list of arrays from /proc/mdstat
|
|
devices, err := listArrayDevices(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list array devices: %w", err)
|
|
}
|
|
|
|
if len(devices) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Collect detailed info for each array
|
|
var arrays []host.RAIDArray
|
|
for _, device := range devices {
|
|
array, err := collectArrayDetail(ctx, device)
|
|
if err != nil {
|
|
// Log but don't fail - continue with other arrays
|
|
continue
|
|
}
|
|
arrays = append(arrays, array)
|
|
}
|
|
|
|
return arrays, nil
|
|
}
|
|
|
|
// isMdadmAvailable checks if mdadm binary is accessible
|
|
func isMdadmAvailable(ctx context.Context) bool {
|
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := runCommandOutput(ctx, "mdadm", "--version")
|
|
return err == nil
|
|
}
|
|
|
|
// listArrayDevices scans /proc/mdstat to find all md devices
|
|
func listArrayDevices(ctx context.Context) ([]string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
defer cancel()
|
|
|
|
output, err := runCommandOutput(ctx, "cat", "/proc/mdstat")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read /proc/mdstat: %w", err)
|
|
}
|
|
|
|
// Parse /proc/mdstat to find device names
|
|
// Lines like: md0 : active raid1 sdb1[1] sda1[0]
|
|
var devices []string
|
|
for _, line := range strings.Split(string(output), "\n") {
|
|
matches := mdDeviceRe.FindStringSubmatch(line)
|
|
if len(matches) > 1 {
|
|
devices = append(devices, "/dev/"+matches[1])
|
|
}
|
|
}
|
|
|
|
return devices, nil
|
|
}
|
|
|
|
// collectArrayDetail runs mdadm --detail on a specific device and parses the output
|
|
func collectArrayDetail(ctx context.Context, device string) (host.RAIDArray, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
output, err := runCommandOutput(ctx, "mdadm", "--detail", device)
|
|
if err != nil {
|
|
return host.RAIDArray{}, fmt.Errorf("mdadm --detail %s: %w", device, err)
|
|
}
|
|
|
|
return parseDetail(device, string(output))
|
|
}
|
|
|
|
// parseDetail parses the output of mdadm --detail
|
|
func parseDetail(device, output string) (host.RAIDArray, error) {
|
|
array := host.RAIDArray{
|
|
Device: device,
|
|
Devices: []host.RAIDDevice{},
|
|
}
|
|
|
|
lines := strings.Split(output, "\n")
|
|
inDeviceSection := false
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Skip empty lines
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// Check if we're entering the device list section
|
|
if strings.Contains(line, "Number") && strings.Contains(line, "Major") && strings.Contains(line, "Minor") {
|
|
inDeviceSection = true
|
|
continue
|
|
}
|
|
|
|
// Parse device entries
|
|
if inDeviceSection {
|
|
matches := slotRe.FindStringSubmatch(line)
|
|
if len(matches) >= 7 {
|
|
slot, _ := strconv.Atoi(matches[1])
|
|
state := strings.TrimSpace(matches[5])
|
|
devicePath := strings.TrimSpace(matches[6])
|
|
|
|
array.Devices = append(array.Devices, host.RAIDDevice{
|
|
Device: devicePath,
|
|
State: state,
|
|
Slot: slot,
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Handle spare/faulty devices (different format)
|
|
if strings.Contains(line, "spare") || strings.Contains(line, "faulty") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
state := "spare"
|
|
if strings.Contains(line, "faulty") {
|
|
state = "faulty"
|
|
}
|
|
devicePath := parts[len(parts)-1]
|
|
|
|
array.Devices = append(array.Devices, host.RAIDDevice{
|
|
Device: devicePath,
|
|
State: state,
|
|
Slot: -1,
|
|
})
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Parse key-value pairs
|
|
if strings.Contains(line, ":") {
|
|
// SplitN with n=2 always returns 2 elements when ":" exists
|
|
parts := strings.SplitN(line, ":", 2)
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
|
|
switch key {
|
|
case "Name":
|
|
array.Name = value
|
|
case "Raid Level":
|
|
array.Level = strings.ToLower(value)
|
|
case "State":
|
|
array.State = strings.ToLower(value)
|
|
case "Total Devices":
|
|
array.TotalDevices, _ = strconv.Atoi(value)
|
|
case "Active Devices":
|
|
array.ActiveDevices, _ = strconv.Atoi(value)
|
|
case "Working Devices":
|
|
array.WorkingDevices, _ = strconv.Atoi(value)
|
|
case "Failed Devices":
|
|
array.FailedDevices, _ = strconv.Atoi(value)
|
|
case "Spare Devices":
|
|
array.SpareDevices, _ = strconv.Atoi(value)
|
|
case "UUID":
|
|
array.UUID = value
|
|
case "Rebuild Status":
|
|
// Parse rebuild percentage
|
|
// Format: "50% complete"
|
|
if strings.Contains(value, "%") {
|
|
percentStr := strings.TrimSpace(strings.Split(value, "%")[0])
|
|
array.RebuildPercent, _ = strconv.ParseFloat(percentStr, 64)
|
|
}
|
|
case "Reshape Status":
|
|
// Handle reshape similarly to rebuild
|
|
if strings.Contains(value, "%") {
|
|
percentStr := strings.TrimSpace(strings.Split(value, "%")[0])
|
|
array.RebuildPercent, _ = strconv.ParseFloat(percentStr, 64)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for rebuild/resync info in /proc/mdstat for speed information
|
|
if array.RebuildPercent > 0 {
|
|
speed := getRebuildSpeed(device)
|
|
if speed != "" {
|
|
array.RebuildSpeed = speed
|
|
}
|
|
}
|
|
|
|
return array, nil
|
|
}
|
|
|
|
// getRebuildSpeed extracts rebuild speed from /proc/mdstat
|
|
func getRebuildSpeed(device string) string {
|
|
// Remove /dev/ prefix for /proc/mdstat lookup
|
|
deviceName := strings.TrimPrefix(device, "/dev/")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
output, err := runCommandOutput(ctx, "cat", "/proc/mdstat")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
// Look for lines containing rebuild/resync speed
|
|
// Example: [==>..................] recovery = 12.6% (37043392/293039104) finish=127.5min speed=33440K/sec
|
|
lines := strings.Split(string(output), "\n")
|
|
inSection := false
|
|
|
|
for _, line := range lines {
|
|
// Check if this is our device
|
|
if strings.HasPrefix(strings.TrimSpace(line), deviceName) {
|
|
inSection = true
|
|
continue
|
|
}
|
|
|
|
// If we're in the right section, look for speed info
|
|
if inSection {
|
|
if strings.Contains(line, "speed=") {
|
|
// Extract speed value using pre-compiled regex
|
|
matches := speedRe.FindStringSubmatch(line)
|
|
if len(matches) > 1 {
|
|
return matches[1]
|
|
}
|
|
}
|
|
|
|
// Exit section when we hit a new device or blank line
|
|
if strings.TrimSpace(line) == "" || (strings.HasPrefix(strings.TrimSpace(line), "md") && strings.Contains(line, ":")) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|