1
2
3
4
5
6
7
8
9
10
11
12
13
14 package procfs
15
16 import (
17 "fmt"
18 "os"
19 "regexp"
20 "strconv"
21 "strings"
22 )
23
24 var (
25 statusLineRE = regexp.MustCompile(`(\d+) blocks .*\[(\d+)/(\d+)\] \[([U_]+)\]`)
26 recoveryLineBlocksRE = regexp.MustCompile(`\((\d+)/\d+\)`)
27 recoveryLinePctRE = regexp.MustCompile(`= (.+)%`)
28 recoveryLineFinishRE = regexp.MustCompile(`finish=(.+)min`)
29 recoveryLineSpeedRE = regexp.MustCompile(`speed=(.+)[A-Z]`)
30 componentDeviceRE = regexp.MustCompile(`(.*)\[\d+\]`)
31 )
32
33
34 type MDStat struct {
35
36 Name string
37
38 ActivityState string
39
40 DisksActive int64
41
42 DisksTotal int64
43
44 DisksFailed int64
45
46 DisksDown int64
47
48 DisksSpare int64
49
50 BlocksTotal int64
51
52 BlocksSynced int64
53
54 BlocksSyncedPct float64
55
56 BlocksSyncedFinishTime float64
57
58 BlocksSyncedSpeed float64
59
60 Devices []string
61 }
62
63
64
65
66 func (fs FS) MDStat() ([]MDStat, error) {
67 data, err := os.ReadFile(fs.proc.Path("mdstat"))
68 if err != nil {
69 return nil, err
70 }
71 mdstat, err := parseMDStat(data)
72 if err != nil {
73 return nil, fmt.Errorf("%s: Cannot parse %v: %w", ErrFileParse, fs.proc.Path("mdstat"), err)
74 }
75 return mdstat, nil
76 }
77
78
79
80 func parseMDStat(mdStatData []byte) ([]MDStat, error) {
81 mdStats := []MDStat{}
82 lines := strings.Split(string(mdStatData), "\n")
83
84 for i, line := range lines {
85 if strings.TrimSpace(line) == "" || line[0] == ' ' ||
86 strings.HasPrefix(line, "Personalities") ||
87 strings.HasPrefix(line, "unused") {
88 continue
89 }
90
91 deviceFields := strings.Fields(line)
92 if len(deviceFields) < 3 {
93 return nil, fmt.Errorf("%s: Expected 3+ lines, got %q", ErrFileParse, line)
94 }
95 mdName := deviceFields[0]
96 state := deviceFields[2]
97
98 if len(lines) <= i+3 {
99 return nil, fmt.Errorf("%w: Too few lines for md device: %q", ErrFileParse, mdName)
100 }
101
102
103 fail := int64(strings.Count(line, "(F)"))
104 spare := int64(strings.Count(line, "(S)"))
105 active, total, down, size, err := evalStatusLine(lines[i], lines[i+1])
106
107 if err != nil {
108 return nil, fmt.Errorf("%s: Cannot parse md device lines: %v: %w", ErrFileParse, active, err)
109 }
110
111 syncLineIdx := i + 2
112 if strings.Contains(lines[i+2], "bitmap") {
113 syncLineIdx++
114 }
115
116
117
118 syncedBlocks := size
119 speed := float64(0)
120 finish := float64(0)
121 pct := float64(0)
122 recovering := strings.Contains(lines[syncLineIdx], "recovery")
123 resyncing := strings.Contains(lines[syncLineIdx], "resync")
124 checking := strings.Contains(lines[syncLineIdx], "check")
125
126
127 if recovering || resyncing || checking {
128 if recovering {
129 state = "recovering"
130 } else if checking {
131 state = "checking"
132 } else {
133 state = "resyncing"
134 }
135
136
137 if strings.Contains(lines[syncLineIdx], "PENDING") ||
138 strings.Contains(lines[syncLineIdx], "DELAYED") {
139 syncedBlocks = 0
140 } else {
141 syncedBlocks, pct, finish, speed, err = evalRecoveryLine(lines[syncLineIdx])
142 if err != nil {
143 return nil, fmt.Errorf("%s: Cannot parse sync line in md device: %q: %w", ErrFileParse, mdName, err)
144 }
145 }
146 }
147
148 mdStats = append(mdStats, MDStat{
149 Name: mdName,
150 ActivityState: state,
151 DisksActive: active,
152 DisksFailed: fail,
153 DisksDown: down,
154 DisksSpare: spare,
155 DisksTotal: total,
156 BlocksTotal: size,
157 BlocksSynced: syncedBlocks,
158 BlocksSyncedPct: pct,
159 BlocksSyncedFinishTime: finish,
160 BlocksSyncedSpeed: speed,
161 Devices: evalComponentDevices(deviceFields),
162 })
163 }
164
165 return mdStats, nil
166 }
167
168 func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) {
169 statusFields := strings.Fields(statusLine)
170 if len(statusFields) < 1 {
171 return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
172 }
173
174 sizeStr := statusFields[0]
175 size, err = strconv.ParseInt(sizeStr, 10, 64)
176 if err != nil {
177 return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
178 }
179
180 if strings.Contains(deviceLine, "raid0") || strings.Contains(deviceLine, "linear") {
181
182 total = int64(strings.Count(deviceLine, "["))
183 return total, total, 0, size, nil
184 }
185
186 if strings.Contains(deviceLine, "inactive") {
187 return 0, 0, 0, size, nil
188 }
189
190 matches := statusLineRE.FindStringSubmatch(statusLine)
191 if len(matches) != 5 {
192 return 0, 0, 0, 0, fmt.Errorf("%s: Could not fild all substring matches %s: %w", ErrFileParse, statusLine, err)
193 }
194
195 total, err = strconv.ParseInt(matches[2], 10, 64)
196 if err != nil {
197 return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
198 }
199
200 active, err = strconv.ParseInt(matches[3], 10, 64)
201 if err != nil {
202 return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected active %d: %w", ErrFileParse, active, err)
203 }
204 down = int64(strings.Count(matches[4], "_"))
205
206 return active, total, down, size, nil
207 }
208
209 func evalRecoveryLine(recoveryLine string) (syncedBlocks int64, pct float64, finish float64, speed float64, err error) {
210 matches := recoveryLineBlocksRE.FindStringSubmatch(recoveryLine)
211 if len(matches) != 2 {
212 return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected recoveryLine %s: %w", ErrFileParse, recoveryLine, err)
213 }
214
215 syncedBlocks, err = strconv.ParseInt(matches[1], 10, 64)
216 if err != nil {
217 return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected parsing of recoveryLine %q: %w", ErrFileParse, recoveryLine, err)
218 }
219
220
221 matches = recoveryLinePctRE.FindStringSubmatch(recoveryLine)
222 if len(matches) != 2 {
223 return syncedBlocks, 0, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching percentage %s", ErrFileParse, recoveryLine)
224 }
225 pct, err = strconv.ParseFloat(strings.TrimSpace(matches[1]), 64)
226 if err != nil {
227 return syncedBlocks, 0, 0, 0, fmt.Errorf("%w: Error parsing float from recoveryLine %q", ErrFileParse, recoveryLine)
228 }
229
230
231 matches = recoveryLineFinishRE.FindStringSubmatch(recoveryLine)
232 if len(matches) != 2 {
233 return syncedBlocks, pct, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching est. finish time: %s", ErrFileParse, recoveryLine)
234 }
235 finish, err = strconv.ParseFloat(matches[1], 64)
236 if err != nil {
237 return syncedBlocks, pct, 0, 0, fmt.Errorf("%w: Unable to parse float from recoveryLine: %q", ErrFileParse, recoveryLine)
238 }
239
240
241 matches = recoveryLineSpeedRE.FindStringSubmatch(recoveryLine)
242 if len(matches) != 2 {
243 return syncedBlocks, pct, finish, 0, fmt.Errorf("%w: Unexpected recoveryLine value: %s", ErrFileParse, recoveryLine)
244 }
245 speed, err = strconv.ParseFloat(matches[1], 64)
246 if err != nil {
247 return syncedBlocks, pct, finish, 0, fmt.Errorf("%s: Error parsing float from recoveryLine: %q: %w", ErrFileParse, recoveryLine, err)
248 }
249
250 return syncedBlocks, pct, finish, speed, nil
251 }
252
253 func evalComponentDevices(deviceFields []string) []string {
254 mdComponentDevices := make([]string, 0)
255 if len(deviceFields) > 3 {
256 for _, field := range deviceFields[4:] {
257 match := componentDeviceRE.FindStringSubmatch(field)
258 if match == nil {
259 continue
260 }
261 mdComponentDevices = append(mdComponentDevices, match[1])
262 }
263 }
264
265 return mdComponentDevices
266 }
267
View as plain text