|
|
|
|
| package agent
|
|
|
| import (
|
| "fmt"
|
| "os"
|
| "path/filepath"
|
| "strconv"
|
| "strings"
|
|
|
| "github.com/henrygd/beszel/agent/utils"
|
| "github.com/henrygd/beszel/internal/entities/smart"
|
| )
|
|
|
|
|
| var mdraidSysfsRoot = "/sys"
|
|
|
| type mdraidHealth struct {
|
| level string
|
| arrayState string
|
| degraded uint64
|
| raidDisks uint64
|
| syncAction string
|
| syncCompleted string
|
| syncSpeed string
|
| mismatchCnt uint64
|
| capacity uint64
|
| }
|
|
|
|
|
| func scanMdraidDevices() []*DeviceInfo {
|
| blockDir := filepath.Join(mdraidSysfsRoot, "block")
|
| entries, err := os.ReadDir(blockDir)
|
| if err != nil {
|
| return nil
|
| }
|
|
|
| devices := make([]*DeviceInfo, 0, 2)
|
| for _, ent := range entries {
|
| name := ent.Name()
|
| if !isMdraidBlockName(name) {
|
| continue
|
| }
|
| mdDir := filepath.Join(blockDir, name, "md")
|
| if !utils.FileExists(filepath.Join(mdDir, "array_state")) {
|
| continue
|
| }
|
|
|
| devPath := filepath.Join("/dev", name)
|
| devices = append(devices, &DeviceInfo{
|
| Name: devPath,
|
| Type: "mdraid",
|
| InfoName: devPath + " [mdraid]",
|
| Protocol: "MD",
|
| })
|
| }
|
|
|
| return devices
|
| }
|
|
|
|
|
| func (sm *SmartManager) collectMdraidHealth(deviceInfo *DeviceInfo) (bool, error) {
|
| if deviceInfo == nil || deviceInfo.Name == "" {
|
| return false, nil
|
| }
|
|
|
| base := filepath.Base(deviceInfo.Name)
|
| if !isMdraidBlockName(base) && !strings.EqualFold(deviceInfo.Type, "mdraid") {
|
| return false, nil
|
| }
|
|
|
| health, ok := readMdraidHealth(base)
|
| if !ok {
|
| return false, nil
|
| }
|
|
|
| deviceInfo.Type = "mdraid"
|
| key := fmt.Sprintf("mdraid:%s", base)
|
| status := mdraidSmartStatus(health)
|
|
|
| attrs := make([]*smart.SmartAttribute, 0, 10)
|
| if health.arrayState != "" {
|
| attrs = append(attrs, &smart.SmartAttribute{Name: "ArrayState", RawString: health.arrayState})
|
| }
|
| if health.level != "" {
|
| attrs = append(attrs, &smart.SmartAttribute{Name: "RaidLevel", RawString: health.level})
|
| }
|
| if health.raidDisks > 0 {
|
| attrs = append(attrs, &smart.SmartAttribute{Name: "RaidDisks", RawValue: health.raidDisks})
|
| }
|
| if health.degraded > 0 {
|
| attrs = append(attrs, &smart.SmartAttribute{Name: "Degraded", RawValue: health.degraded})
|
| }
|
| if health.syncAction != "" {
|
| attrs = append(attrs, &smart.SmartAttribute{Name: "SyncAction", RawString: health.syncAction})
|
| }
|
| if health.syncCompleted != "" {
|
| attrs = append(attrs, &smart.SmartAttribute{Name: "SyncCompleted", RawString: health.syncCompleted})
|
| }
|
| if health.syncSpeed != "" {
|
| attrs = append(attrs, &smart.SmartAttribute{Name: "SyncSpeed", RawString: health.syncSpeed})
|
| }
|
| if health.mismatchCnt > 0 {
|
| attrs = append(attrs, &smart.SmartAttribute{Name: "MismatchCount", RawValue: health.mismatchCnt})
|
| }
|
|
|
| sm.Lock()
|
| defer sm.Unlock()
|
|
|
| if _, exists := sm.SmartDataMap[key]; !exists {
|
| sm.SmartDataMap[key] = &smart.SmartData{}
|
| }
|
|
|
| data := sm.SmartDataMap[key]
|
| data.ModelName = "Linux MD RAID"
|
| if health.level != "" {
|
| data.ModelName = "Linux MD RAID (" + health.level + ")"
|
| }
|
| data.Capacity = health.capacity
|
| data.SmartStatus = status
|
| data.DiskName = filepath.Join("/dev", base)
|
| data.DiskType = "mdraid"
|
| data.Attributes = attrs
|
|
|
| return true, nil
|
| }
|
|
|
|
|
| func readMdraidHealth(blockName string) (mdraidHealth, bool) {
|
| var out mdraidHealth
|
|
|
| if !isMdraidBlockName(blockName) {
|
| return out, false
|
| }
|
|
|
| mdDir := filepath.Join(mdraidSysfsRoot, "block", blockName, "md")
|
| arrayState, okState := utils.ReadStringFileOK(filepath.Join(mdDir, "array_state"))
|
| if !okState {
|
| return out, false
|
| }
|
|
|
| out.arrayState = arrayState
|
| out.level = utils.ReadStringFile(filepath.Join(mdDir, "level"))
|
| out.syncAction = utils.ReadStringFile(filepath.Join(mdDir, "sync_action"))
|
| out.syncCompleted = utils.ReadStringFile(filepath.Join(mdDir, "sync_completed"))
|
| out.syncSpeed = utils.ReadStringFile(filepath.Join(mdDir, "sync_speed"))
|
|
|
| if val, ok := utils.ReadUintFile(filepath.Join(mdDir, "raid_disks")); ok {
|
| out.raidDisks = val
|
| }
|
| if val, ok := utils.ReadUintFile(filepath.Join(mdDir, "degraded")); ok {
|
| out.degraded = val
|
| }
|
| if val, ok := utils.ReadUintFile(filepath.Join(mdDir, "mismatch_cnt")); ok {
|
| out.mismatchCnt = val
|
| }
|
|
|
| if capBytes, ok := readMdraidBlockCapacityBytes(blockName, mdraidSysfsRoot); ok {
|
| out.capacity = capBytes
|
| }
|
|
|
| return out, true
|
| }
|
|
|
|
|
| func mdraidSmartStatus(health mdraidHealth) string {
|
| state := strings.ToLower(strings.TrimSpace(health.arrayState))
|
| switch state {
|
| case "inactive", "faulty", "broken", "stopped":
|
| return "FAILED"
|
| }
|
|
|
|
|
| syncAction := strings.ToLower(strings.TrimSpace(health.syncAction))
|
| switch syncAction {
|
| case "resync", "recover", "reshape":
|
| return "WARNING"
|
| }
|
| if health.degraded > 0 {
|
| return "FAILED"
|
| }
|
| switch syncAction {
|
| case "check", "repair":
|
| return "WARNING"
|
| }
|
| switch state {
|
| case "clean", "active", "active-idle", "write-pending", "read-auto", "readonly":
|
| return "PASSED"
|
| }
|
| return "UNKNOWN"
|
| }
|
|
|
|
|
| func isMdraidBlockName(name string) bool {
|
| if !strings.HasPrefix(name, "md") {
|
| return false
|
| }
|
| suffix := strings.TrimPrefix(name, "md")
|
| if suffix == "" {
|
| return false
|
| }
|
| for _, c := range suffix {
|
| if c < '0' || c > '9' {
|
| return false
|
| }
|
| }
|
| return true
|
| }
|
|
|
|
|
| func readMdraidBlockCapacityBytes(blockName, root string) (uint64, bool) {
|
| sizePath := filepath.Join(root, "block", blockName, "size")
|
| lbsPath := filepath.Join(root, "block", blockName, "queue", "logical_block_size")
|
|
|
| sizeStr, ok := utils.ReadStringFileOK(sizePath)
|
| if !ok {
|
| return 0, false
|
| }
|
| sectors, err := strconv.ParseUint(sizeStr, 10, 64)
|
| if err != nil || sectors == 0 {
|
| return 0, false
|
| }
|
|
|
| logicalBlockSize := uint64(512)
|
| if lbsStr, ok := utils.ReadStringFileOK(lbsPath); ok {
|
| if parsed, err := strconv.ParseUint(lbsStr, 10, 64); err == nil && parsed > 0 {
|
| logicalBlockSize = parsed
|
| }
|
| }
|
|
|
| return sectors * logicalBlockSize, true
|
| }
|
|
|