package procfs
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
)
var (
statusLineRE = regexp .MustCompile (`(\d+) blocks .*\[(\d+)/(\d+)\] \[([U_]+)\]` )
recoveryLineBlocksRE = regexp .MustCompile (`\((\d+/\d+)\)` )
recoveryLinePctRE = regexp .MustCompile (`= (.+)%` )
recoveryLineFinishRE = regexp .MustCompile (`finish=(.+)min` )
recoveryLineSpeedRE = regexp .MustCompile (`speed=(.+)[A-Z]` )
componentDeviceRE = regexp .MustCompile (`(.*)\[\d+\]` )
)
type MDStat struct {
Name string
ActivityState string
DisksActive int64
DisksTotal int64
DisksFailed int64
DisksDown int64
DisksSpare int64
BlocksTotal int64
BlocksSynced int64
BlocksToBeSynced int64
BlocksSyncedPct float64
BlocksSyncedFinishTime float64
BlocksSyncedSpeed float64
Devices []string
}
func (fs FS ) MDStat () ([]MDStat , error ) {
data , err := os .ReadFile (fs .proc .Path ("mdstat" ))
if err != nil {
return nil , err
}
mdstat , err := parseMDStat (data )
if err != nil {
return nil , fmt .Errorf ("%w: Cannot parse %v: %w" , ErrFileParse , fs .proc .Path ("mdstat" ), err )
}
return mdstat , nil
}
func parseMDStat(mdStatData []byte ) ([]MDStat , error ) {
mdStats := []MDStat {}
lines := strings .Split (string (mdStatData ), "\n" )
for i , line := range lines {
if strings .TrimSpace (line ) == "" || line [0 ] == ' ' ||
strings .HasPrefix (line , "Personalities" ) ||
strings .HasPrefix (line , "unused" ) {
continue
}
deviceFields := strings .Fields (line )
if len (deviceFields ) < 3 {
return nil , fmt .Errorf ("%w: Expected 3+ lines, got %q" , ErrFileParse , line )
}
mdName := deviceFields [0 ]
state := deviceFields [2 ]
if len (lines ) <= i +3 {
return nil , fmt .Errorf ("%w: Too few lines for md device: %q" , ErrFileParse , mdName )
}
fail := int64 (strings .Count (line , "(F)" ))
spare := int64 (strings .Count (line , "(S)" ))
active , total , down , size , err := evalStatusLine (lines [i ], lines [i +1 ])
if err != nil {
return nil , fmt .Errorf ("%w: Cannot parse md device lines: %v: %w" , ErrFileParse , active , err )
}
syncLineIdx := i + 2
if strings .Contains (lines [i +2 ], "bitmap" ) {
syncLineIdx ++
}
blocksSynced := size
blocksToBeSynced := size
speed := float64 (0 )
finish := float64 (0 )
pct := float64 (0 )
recovering := strings .Contains (lines [syncLineIdx ], "recovery" )
resyncing := strings .Contains (lines [syncLineIdx ], "resync" )
checking := strings .Contains (lines [syncLineIdx ], "check" )
if recovering || resyncing || checking {
if recovering {
state = "recovering"
} else if checking {
state = "checking"
} else {
state = "resyncing"
}
if strings .Contains (lines [syncLineIdx ], "PENDING" ) ||
strings .Contains (lines [syncLineIdx ], "DELAYED" ) {
blocksSynced = 0
} else {
blocksSynced , blocksToBeSynced , pct , finish , speed , err = evalRecoveryLine (lines [syncLineIdx ])
if err != nil {
return nil , fmt .Errorf ("%w: Cannot parse sync line in md device: %q: %w" , ErrFileParse , mdName , err )
}
}
}
mdStats = append (mdStats , MDStat {
Name : mdName ,
ActivityState : state ,
DisksActive : active ,
DisksFailed : fail ,
DisksDown : down ,
DisksSpare : spare ,
DisksTotal : total ,
BlocksTotal : size ,
BlocksSynced : blocksSynced ,
BlocksToBeSynced : blocksToBeSynced ,
BlocksSyncedPct : pct ,
BlocksSyncedFinishTime : finish ,
BlocksSyncedSpeed : speed ,
Devices : evalComponentDevices (deviceFields ),
})
}
return mdStats , nil
}
func evalStatusLine(deviceLine , statusLine string ) (active , total , down , size int64 , err error ) {
statusFields := strings .Fields (statusLine )
if len (statusFields ) < 1 {
return 0 , 0 , 0 , 0 , fmt .Errorf ("%w: Unexpected statusline %q: %w" , ErrFileParse , statusLine , err )
}
sizeStr := statusFields [0 ]
size , err = strconv .ParseInt (sizeStr , 10 , 64 )
if err != nil {
return 0 , 0 , 0 , 0 , fmt .Errorf ("%w: Unexpected statusline %q: %w" , ErrFileParse , statusLine , err )
}
if strings .Contains (deviceLine , "raid0" ) || strings .Contains (deviceLine , "linear" ) {
total = int64 (strings .Count (deviceLine , "[" ))
return total , total , 0 , size , nil
}
if strings .Contains (deviceLine , "inactive" ) {
return 0 , 0 , 0 , size , nil
}
matches := statusLineRE .FindStringSubmatch (statusLine )
if len (matches ) != 5 {
return 0 , 0 , 0 , 0 , fmt .Errorf ("%w: Could not fild all substring matches %s: %w" , ErrFileParse , statusLine , err )
}
total , err = strconv .ParseInt (matches [2 ], 10 , 64 )
if err != nil {
return 0 , 0 , 0 , 0 , fmt .Errorf ("%w: Unexpected statusline %q: %w" , ErrFileParse , statusLine , err )
}
active , err = strconv .ParseInt (matches [3 ], 10 , 64 )
if err != nil {
return 0 , 0 , 0 , 0 , fmt .Errorf ("%w: Unexpected active %d: %w" , ErrFileParse , active , err )
}
down = int64 (strings .Count (matches [4 ], "_" ))
return active , total , down , size , nil
}
func evalRecoveryLine(recoveryLine string ) (blocksSynced int64 , blocksToBeSynced int64 , pct float64 , finish float64 , speed float64 , err error ) {
matches := recoveryLineBlocksRE .FindStringSubmatch (recoveryLine )
if len (matches ) != 2 {
return 0 , 0 , 0 , 0 , 0 , fmt .Errorf ("%w: Unexpected recoveryLine blocks %s: %w" , ErrFileParse , recoveryLine , err )
}
blocks := strings .Split (matches [1 ], "/" )
blocksSynced , err = strconv .ParseInt (blocks [0 ], 10 , 64 )
if err != nil {
return 0 , 0 , 0 , 0 , 0 , fmt .Errorf ("%w: Unable to parse recovery blocks synced %q: %w" , ErrFileParse , matches [1 ], err )
}
blocksToBeSynced , err = strconv .ParseInt (blocks [1 ], 10 , 64 )
if err != nil {
return blocksSynced , 0 , 0 , 0 , 0 , fmt .Errorf ("%w: Unable to parse recovery to be synced blocks %q: %w" , ErrFileParse , matches [2 ], err )
}
matches = recoveryLinePctRE .FindStringSubmatch (recoveryLine )
if len (matches ) != 2 {
return blocksSynced , blocksToBeSynced , 0 , 0 , 0 , fmt .Errorf ("%w: Unexpected recoveryLine matching percentage %s" , ErrFileParse , recoveryLine )
}
pct , err = strconv .ParseFloat (strings .TrimSpace (matches [1 ]), 64 )
if err != nil {
return blocksSynced , blocksToBeSynced , 0 , 0 , 0 , fmt .Errorf ("%w: Error parsing float from recoveryLine %q" , ErrFileParse , recoveryLine )
}
matches = recoveryLineFinishRE .FindStringSubmatch (recoveryLine )
if len (matches ) != 2 {
return blocksSynced , blocksToBeSynced , pct , 0 , 0 , fmt .Errorf ("%w: Unexpected recoveryLine matching est. finish time: %s" , ErrFileParse , recoveryLine )
}
finish , err = strconv .ParseFloat (matches [1 ], 64 )
if err != nil {
return blocksSynced , blocksToBeSynced , pct , 0 , 0 , fmt .Errorf ("%w: Unable to parse float from recoveryLine: %q" , ErrFileParse , recoveryLine )
}
matches = recoveryLineSpeedRE .FindStringSubmatch (recoveryLine )
if len (matches ) != 2 {
return blocksSynced , blocksToBeSynced , pct , finish , 0 , fmt .Errorf ("%w: Unexpected recoveryLine value: %s" , ErrFileParse , recoveryLine )
}
speed , err = strconv .ParseFloat (matches [1 ], 64 )
if err != nil {
return blocksSynced , blocksToBeSynced , pct , finish , 0 , fmt .Errorf ("%w: Error parsing float from recoveryLine: %q: %w" , ErrFileParse , recoveryLine , err )
}
return blocksSynced , blocksToBeSynced , pct , finish , speed , nil
}
func evalComponentDevices(deviceFields []string ) []string {
mdComponentDevices := make ([]string , 0 )
if len (deviceFields ) > 3 {
for _ , field := range deviceFields [4 :] {
match := componentDeviceRE .FindStringSubmatch (field )
if match == nil {
continue
}
mdComponentDevices = append (mdComponentDevices , match [1 ])
}
}
return mdComponentDevices
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .