diff --git a/mdstat.go b/mdstat.go index 67a9d2b4..e39f639d 100644 --- a/mdstat.go +++ b/mdstat.go @@ -27,13 +27,34 @@ var ( recoveryLinePctRE = regexp.MustCompile(`= (.+)%`) recoveryLineFinishRE = regexp.MustCompile(`finish=(.+)min`) recoveryLineSpeedRE = regexp.MustCompile(`speed=(.+)[A-Z]`) - componentDeviceRE = regexp.MustCompile(`(.*)\[\d+\]`) + componentDeviceRE = regexp.MustCompile(`(.*)\[(\d+)\](\([SF]+\))?`) + personalitiesPrefix = "Personalities : " ) +type MDStatComponent struct { + // Name of the component device. + Name string + // DescriptorIndex number of component device, e.g. the order in the superblock. + DescriptorIndex int32 + // Flags per Linux drivers/md/md.[ch] as of v6.12-rc1 + // Subset that are exposed in mdstat + WriteMostly bool + Journal bool + Faulty bool // "Faulty" is what kernel source uses for "(F)" + Spare bool + Replacement bool + // Some additional flags that are NOT exposed in procfs today; they may + // be available via sysfs. + // In_sync, Bitmap_sync, Blocked, WriteErrorSeen, FaultRecorded, + // BlockedBadBlocks, WantReplacement, Candidate, ... +} + // MDStat holds info parsed from /proc/mdstat. type MDStat struct { // Name of the device. Name string + // raid type of the device. + Type string // activity-state of the device. ActivityState string // Number of active disks. @@ -58,8 +79,8 @@ type MDStat struct { BlocksSyncedFinishTime float64 // current sync speed (in Kilobytes/sec) BlocksSyncedSpeed float64 - // Name of md component devices - Devices []string + // component devices + Devices []MDStatComponent } // MDStat parses an mdstat-file (/proc/mdstat) and returns a slice of @@ -80,28 +101,52 @@ func (fs FS) MDStat() ([]MDStat, error) { // parseMDStat parses data from mdstat file (/proc/mdstat) and returns a slice of // structs containing the relevant info. func parseMDStat(mdStatData []byte) ([]MDStat, error) { + // TODO: + // - parse global hotspares from the "unused devices" line. mdStats := []MDStat{} lines := strings.Split(string(mdStatData), "\n") + knownRaidTypes := make(map[string]bool) for i, line := range lines { if strings.TrimSpace(line) == "" || line[0] == ' ' || - strings.HasPrefix(line, "Personalities") || strings.HasPrefix(line, "unused") { continue } + // Personalities : [linear] [multipath] [raid0] [raid1] [raid6] [raid5] [raid4] [raid10] + if len(knownRaidTypes) == 0 && strings.HasPrefix(line, personalitiesPrefix) { + personalities := strings.Fields(line[len(personalitiesPrefix):]) + for _, word := range personalities { + word := word[1 : len(word)-1] + knownRaidTypes[word] = true + } + continue + } deviceFields := strings.Fields(line) if len(deviceFields) < 3 { return nil, fmt.Errorf("%w: Expected 3+ lines, got %q", ErrFileParse, line) } mdName := deviceFields[0] // mdx - state := deviceFields[2] // active or inactive + state := deviceFields[2] // active, inactive, broken + + mdType := "unknown" // raid1, raid5, etc. + var deviceStartIndex int + if len(deviceFields) > 3 { // mdType may be in the 3rd or 4th field + if isRaidType(deviceFields[3], knownRaidTypes) { + mdType = deviceFields[3] + deviceStartIndex = 4 + } else if len(deviceFields) > 4 && isRaidType(deviceFields[4], knownRaidTypes) { + // if the 3rd field is (...), the 4th field is the mdType + mdType = deviceFields[4] + deviceStartIndex = 5 + } + } if len(lines) <= i+3 { return nil, fmt.Errorf("%w: Too few lines for md device: %q", ErrFileParse, mdName) } - // Failed disks have the suffix (F) & Spare disks have the suffix (S). + // Failed (Faulty) disks have the suffix (F) & Spare disks have the suffix (S). fail := int64(strings.Count(line, "(F)")) spare := int64(strings.Count(line, "(S)")) active, total, down, size, err := evalStatusLine(lines[i], lines[i+1]) @@ -148,8 +193,14 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) { } } + devices, err := evalComponentDevices(deviceFields[deviceStartIndex:]) + if err != nil { + return nil, fmt.Errorf("error parsing components in md device %q: %w", mdName, err) + } + mdStats = append(mdStats, MDStat{ Name: mdName, + Type: mdType, ActivityState: state, DisksActive: active, DisksFailed: fail, @@ -162,14 +213,23 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) { BlocksSyncedPct: pct, BlocksSyncedFinishTime: finish, BlocksSyncedSpeed: speed, - Devices: evalComponentDevices(deviceFields), + Devices: devices, }) } return mdStats, nil } +// check if a string's format is like the mdType +// Rule 1: mdType should not be like (...) +// Rule 2: mdType should not be like sda[0] +func isRaidType(mdType string, knownRaidTypes map[string]bool) bool { + _, ok := knownRaidTypes[mdType] + return !strings.ContainsAny(mdType, "([") && ok +} + func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) { + // e.g. 523968 blocks super 1.2 [4/4] [UUUU] statusFields := strings.Fields(statusLine) if len(statusFields) < 1 { return 0, 0, 0, 0, fmt.Errorf("%w: Unexpected statusline %q: %w", ErrFileParse, statusLine, err) @@ -260,17 +320,29 @@ func evalRecoveryLine(recoveryLine string) (blocksSynced int64, blocksToBeSynced 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]) +func evalComponentDevices(deviceFields []string) ([]MDStatComponent, error) { + mdComponentDevices := make([]MDStatComponent, 0) + for _, field := range deviceFields { + match := componentDeviceRE.FindStringSubmatch(field) + if match == nil { + continue + } + descriptorIndex, err := strconv.ParseInt(match[2], 10, 32) + if err != nil { + return mdComponentDevices, fmt.Errorf("error parsing int from device %q: %w", match[2], err) } + mdComponentDevices = append(mdComponentDevices, MDStatComponent{ + Name: match[1], + DescriptorIndex: int32(descriptorIndex), + // match may contain one or more of these + // https://github.com/torvalds/linux/blob/7ec462100ef9142344ddbf86f2c3008b97acddbe/drivers/md/md.c#L8376-L8392 + Faulty: strings.Contains(match[3], "(F)"), + Spare: strings.Contains(match[3], "(S)"), + Journal: strings.Contains(match[3], "(J)"), + Replacement: strings.Contains(match[3], "(R)"), + WriteMostly: strings.Contains(match[3], "(W)"), + }) } - return mdComponentDevices + return mdComponentDevices, nil } diff --git a/mdstat_test.go b/mdstat_test.go index 7a577edc..a2e8b1ab 100644 --- a/mdstat_test.go +++ b/mdstat_test.go @@ -26,10 +26,16 @@ func TestFS_MDStat(t *testing.T) { if err != nil { t.Fatalf("parsing of reference-file failed entirely: %s", err) } + // TODO: Test cases to capture in future: + // WriteMostly devices + // Journal devices + // Replacement devices + // Global hotspares refs := map[string]MDStat{ "md127": { Name: "md127", + Type: "raid1", ActivityState: "active", DisksActive: 2, DisksTotal: 2, @@ -42,9 +48,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdi2", "sdj2"}}, + Devices: []MDStatComponent{{Name: "sdi2", DescriptorIndex: 0}, {Name: "sdj2", DescriptorIndex: 1}}}, "md0": { Name: "md0", + Type: "raid1", ActivityState: "active", DisksActive: 2, DisksTotal: 2, @@ -57,9 +64,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdi1", "sdj1"}}, + Devices: []MDStatComponent{{Name: "sdi1", DescriptorIndex: 0}, {Name: "sdj1", DescriptorIndex: 1}}}, "md4": { Name: "md4", + Type: "raid1", ActivityState: "inactive", DisksActive: 0, DisksTotal: 0, @@ -72,9 +80,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sda3", "sdb3"}}, + Devices: []MDStatComponent{{Name: "sda3", Faulty: true, DescriptorIndex: 0}, {Name: "sdb3", Spare: true, DescriptorIndex: 1}}}, "md6": { Name: "md6", + Type: "raid1", ActivityState: "recovering", DisksActive: 1, DisksTotal: 2, @@ -87,9 +96,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 8.5, BlocksSyncedFinishTime: 17, BlocksSyncedSpeed: 259783, - Devices: []string{"sdb2", "sdc", "sda2"}}, + Devices: []MDStatComponent{{Name: "sdb2", DescriptorIndex: 2, Faulty: true}, {Name: "sdc", DescriptorIndex: 1, Spare: true}, {Name: "sda2", DescriptorIndex: 0}}}, "md3": { Name: "md3", + Type: "raid6", ActivityState: "active", DisksActive: 8, DisksTotal: 8, @@ -102,9 +112,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sda1", "sdh1", "sdg1", "sdf1", "sde1", "sdd1", "sdc1", "sdb1", "sdd1", "sdd2"}}, + Devices: []MDStatComponent{{Name: "sda1", DescriptorIndex: 8}, {Name: "sdh1", DescriptorIndex: 7}, {Name: "sdg1", DescriptorIndex: 6}, {Name: "sdf1", DescriptorIndex: 5}, {Name: "sde1", DescriptorIndex: 11}, {Name: "sdd1", DescriptorIndex: 3}, {Name: "sdc1", DescriptorIndex: 10}, {Name: "sdb1", DescriptorIndex: 9}, {Name: "sdd1", DescriptorIndex: 10, Spare: true}, {Name: "sdd2", DescriptorIndex: 11, Spare: true}}}, "md8": { Name: "md8", + Type: "raid1", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, @@ -117,9 +128,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 8.5, BlocksSyncedFinishTime: 17, BlocksSyncedSpeed: 259783, - Devices: []string{"sdb1", "sda1", "sdc", "sde"}}, + Devices: []MDStatComponent{{Name: "sdb1", DescriptorIndex: 1}, {Name: "sda1", DescriptorIndex: 0}, {Name: "sdc", DescriptorIndex: 2, Spare: true}, {Name: "sde", DescriptorIndex: 3, Spare: true}}}, "md7": { Name: "md7", + Type: "raid6", ActivityState: "active", DisksActive: 3, DisksTotal: 4, @@ -132,9 +144,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdb1", "sde1", "sdd1", "sdc1"}}, + Devices: []MDStatComponent{{Name: "sdb1", DescriptorIndex: 0}, {Name: "sde1", DescriptorIndex: 3}, {Name: "sdd1", DescriptorIndex: 2}, {Name: "sdc1", DescriptorIndex: 1, Faulty: true}}}, "md9": { Name: "md9", + Type: "raid1", ActivityState: "resyncing", DisksActive: 4, DisksTotal: 4, @@ -147,9 +160,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdc2", "sdd2", "sdb2", "sda2", "sde", "sdf", "sdg"}}, + Devices: []MDStatComponent{{Name: "sdc2", DescriptorIndex: 2}, {Name: "sdd2", DescriptorIndex: 3}, {Name: "sdb2", DescriptorIndex: 1}, {Name: "sda2", DescriptorIndex: 0}, {Name: "sde", DescriptorIndex: 4, Faulty: true}, {Name: "sdf", DescriptorIndex: 5, Faulty: true}, {Name: "sdg", DescriptorIndex: 6, Spare: true}}}, "md10": { Name: "md10", + Type: "raid0", ActivityState: "active", DisksActive: 2, DisksTotal: 2, @@ -162,9 +176,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sda1", "sdb1"}}, + Devices: []MDStatComponent{{Name: "sda1", DescriptorIndex: 0}, {Name: "sdb1", DescriptorIndex: 1}}}, "md11": { Name: "md11", + Type: "raid1", ActivityState: "resyncing", DisksActive: 2, DisksTotal: 2, @@ -177,9 +192,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdb2", "sdc2", "sdc3", "hda", "ssdc2"}}, + Devices: []MDStatComponent{{Name: "sdb2", DescriptorIndex: 0}, {Name: "sdc2", DescriptorIndex: 1}, {Name: "sdc3", DescriptorIndex: 2, Faulty: true}, {Name: "hda", DescriptorIndex: 4, Spare: true}, {Name: "ssdc2", DescriptorIndex: 3, Spare: true}}}, "md12": { Name: "md12", + Type: "raid0", ActivityState: "active", DisksActive: 2, DisksTotal: 2, @@ -192,9 +208,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdc2", "sdd2"}}, + Devices: []MDStatComponent{{Name: "sdc2", DescriptorIndex: 0}, {Name: "sdd2", DescriptorIndex: 1}}}, "md120": { Name: "md120", + Type: "linear", ActivityState: "active", DisksActive: 2, DisksTotal: 2, @@ -207,9 +224,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sda1", "sdb1"}}, + Devices: []MDStatComponent{{Name: "sda1", DescriptorIndex: 1}, {Name: "sdb1", DescriptorIndex: 0}}}, "md126": { Name: "md126", + Type: "raid0", ActivityState: "active", DisksActive: 2, DisksTotal: 2, @@ -222,9 +240,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdb", "sdc"}}, + Devices: []MDStatComponent{{Name: "sdb", DescriptorIndex: 1}, {Name: "sdc", DescriptorIndex: 0}}}, "md219": { Name: "md219", + Type: "unknown", ActivityState: "inactive", DisksTotal: 0, DisksFailed: 0, @@ -237,9 +256,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdc", "sda"}}, + Devices: []MDStatComponent{{Name: "sdb", DescriptorIndex: 2, Spare: true}, {Name: "sdc", DescriptorIndex: 1, Spare: true}, {Name: "sda", DescriptorIndex: 0, Spare: true}}}, "md00": { Name: "md00", + Type: "raid0", ActivityState: "active", DisksActive: 1, DisksTotal: 1, @@ -252,9 +272,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"xvdb"}}, + Devices: []MDStatComponent{{Name: "xvdb", DescriptorIndex: 0}}}, "md101": { Name: "md101", + Type: "raid0", ActivityState: "active", DisksActive: 3, DisksTotal: 3, @@ -267,9 +288,10 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 0, BlocksSyncedFinishTime: 0, BlocksSyncedSpeed: 0, - Devices: []string{"sdb", "sdd", "sdc"}}, + Devices: []MDStatComponent{{Name: "sdb", DescriptorIndex: 2}, {Name: "sdd", DescriptorIndex: 1}, {Name: "sdc", DescriptorIndex: 0}}}, "md201": { Name: "md201", + Type: "raid1", ActivityState: "checking", DisksActive: 2, DisksTotal: 2, @@ -282,7 +304,7 @@ func TestFS_MDStat(t *testing.T) { BlocksSyncedPct: 5.7, BlocksSyncedFinishTime: 0.2, BlocksSyncedSpeed: 114176, - Devices: []string{"sda3", "sdb3"}}, + Devices: []MDStatComponent{{Name: "sda3", DescriptorIndex: 0}, {Name: "sdb3", DescriptorIndex: 1}}}, } if want, have := len(refs), len(mdStats); want != have { @@ -297,18 +319,32 @@ func TestFS_MDStat(t *testing.T) { } func TestInvalidMdstat(t *testing.T) { - invalidMount := [][]byte{[]byte(` + invalidMount := [][]byte{ + // Test invalid Personality and format + []byte(` Personalities : [invalid] md3 : invalid 314159265 blocks 64k chunks unused devices: `), + // Test extra blank line []byte(` md12 : active raid0 sdc2[0] sdd2[1] 3886394368 blocks super 1.2 512k chunks -`)} +`), + // test for impossible component state + []byte(` +md127 : active raid1 sdi2[0] sdj2[1](Z) + 312319552 blocks [2/2] [UU] +`), + // test for malformed component state + []byte(` +md127 : active raid1 sdi2[0] sdj2[X] + 312319552 blocks [2/2] [UU] +`), + } for _, invalid := range invalidMount { _, err := parseMDStat(invalid)