1// Copyright 2018 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package procfs
15
16import (
17	"fmt"
18	"io/ioutil"
19	"regexp"
20	"strconv"
21	"strings"
22)
23
24var (
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// MDStat holds info parsed from /proc/mdstat.
34type MDStat struct {
35	// Name of the device.
36	Name string
37	// activity-state of the device.
38	ActivityState string
39	// Number of active disks.
40	DisksActive int64
41	// Total number of disks the device requires.
42	DisksTotal int64
43	// Number of failed disks.
44	DisksFailed int64
45	// Number of "down" disks. (the _ indicator in the status line)
46	DisksDown int64
47	// Spare disks in the device.
48	DisksSpare int64
49	// Number of blocks the device holds.
50	BlocksTotal int64
51	// Number of blocks on the device that are in sync.
52	BlocksSynced int64
53	// progress percentage of current sync
54	BlocksSyncedPct float64
55	// estimated finishing time for current sync (in minutes)
56	BlocksSyncedFinishTime float64
57	// current sync speed (in Kilobytes/sec)
58	BlocksSyncedSpeed float64
59	// Name of md component devices
60	Devices []string
61}
62
63// MDStat parses an mdstat-file (/proc/mdstat) and returns a slice of
64// structs containing the relevant info.  More information available here:
65// https://raid.wiki.kernel.org/index.php/Mdstat
66func (fs FS) MDStat() ([]MDStat, error) {
67	data, err := ioutil.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("error parsing mdstat %q: %w", fs.proc.Path("mdstat"), err)
74	}
75	return mdstat, nil
76}
77
78// parseMDStat parses data from mdstat file (/proc/mdstat) and returns a slice of
79// structs containing the relevant info.
80func 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("not enough fields in mdline (expected at least 3): %s", line)
94		}
95		mdName := deviceFields[0] // mdx
96		state := deviceFields[2]  // active or inactive
97
98		if len(lines) <= i+3 {
99			return nil, fmt.Errorf("error parsing %q: too few lines for md device", mdName)
100		}
101
102		// Failed disks have the suffix (F) & Spare disks have the suffix (S).
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("error parsing md device lines: %w", err)
109		}
110
111		syncLineIdx := i + 2
112		if strings.Contains(lines[i+2], "bitmap") { // skip bitmap line
113			syncLineIdx++
114		}
115
116		// If device is syncing at the moment, get the number of currently
117		// synced bytes, otherwise that number equals the size of the device.
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		// Append recovery and resyncing state info.
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			// Handle case when resync=PENDING or resync=DELAYED.
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("error parsing sync line in md device %q: %w", 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
168func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) {
169
170	sizeStr := strings.Fields(statusLine)[0]
171	size, err = strconv.ParseInt(sizeStr, 10, 64)
172	if err != nil {
173		return 0, 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
174	}
175
176	if strings.Contains(deviceLine, "raid0") || strings.Contains(deviceLine, "linear") {
177		// In the device deviceLine, only disks have a number associated with them in [].
178		total = int64(strings.Count(deviceLine, "["))
179		return total, total, 0, size, nil
180	}
181
182	if strings.Contains(deviceLine, "inactive") {
183		return 0, 0, 0, size, nil
184	}
185
186	matches := statusLineRE.FindStringSubmatch(statusLine)
187	if len(matches) != 5 {
188		return 0, 0, 0, 0, fmt.Errorf("couldn't find all the substring matches: %s", statusLine)
189	}
190
191	total, err = strconv.ParseInt(matches[2], 10, 64)
192	if err != nil {
193		return 0, 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
194	}
195
196	active, err = strconv.ParseInt(matches[3], 10, 64)
197	if err != nil {
198		return 0, 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
199	}
200	down = int64(strings.Count(matches[4], "_"))
201
202	return active, total, down, size, nil
203}
204
205func evalRecoveryLine(recoveryLine string) (syncedBlocks int64, pct float64, finish float64, speed float64, err error) {
206	matches := recoveryLineBlocksRE.FindStringSubmatch(recoveryLine)
207	if len(matches) != 2 {
208		return 0, 0, 0, 0, fmt.Errorf("unexpected recoveryLine: %s", recoveryLine)
209	}
210
211	syncedBlocks, err = strconv.ParseInt(matches[1], 10, 64)
212	if err != nil {
213		return 0, 0, 0, 0, fmt.Errorf("error parsing int from recoveryLine %q: %w", recoveryLine, err)
214	}
215
216	// Get percentage complete
217	matches = recoveryLinePctRE.FindStringSubmatch(recoveryLine)
218	if len(matches) != 2 {
219		return syncedBlocks, 0, 0, 0, fmt.Errorf("unexpected recoveryLine matching percentage: %s", recoveryLine)
220	}
221	pct, err = strconv.ParseFloat(strings.TrimSpace(matches[1]), 64)
222	if err != nil {
223		return syncedBlocks, 0, 0, 0, fmt.Errorf("error parsing float from recoveryLine %q: %w", recoveryLine, err)
224	}
225
226	// Get time expected left to complete
227	matches = recoveryLineFinishRE.FindStringSubmatch(recoveryLine)
228	if len(matches) != 2 {
229		return syncedBlocks, pct, 0, 0, fmt.Errorf("unexpected recoveryLine matching est. finish time: %s", recoveryLine)
230	}
231	finish, err = strconv.ParseFloat(matches[1], 64)
232	if err != nil {
233		return syncedBlocks, pct, 0, 0, fmt.Errorf("error parsing float from recoveryLine %q: %w", recoveryLine, err)
234	}
235
236	// Get recovery speed
237	matches = recoveryLineSpeedRE.FindStringSubmatch(recoveryLine)
238	if len(matches) != 2 {
239		return syncedBlocks, pct, finish, 0, fmt.Errorf("unexpected recoveryLine matching speed: %s", recoveryLine)
240	}
241	speed, err = strconv.ParseFloat(matches[1], 64)
242	if err != nil {
243		return syncedBlocks, pct, finish, 0, fmt.Errorf("error parsing float from recoveryLine %q: %w", recoveryLine, err)
244	}
245
246	return syncedBlocks, pct, finish, speed, nil
247}
248
249func evalComponentDevices(deviceFields []string) []string {
250	mdComponentDevices := make([]string, 0)
251	if len(deviceFields) > 3 {
252		for _, field := range deviceFields[4:] {
253			match := componentDeviceRE.FindStringSubmatch(field)
254			if match == nil {
255				continue
256			}
257			mdComponentDevices = append(mdComponentDevices, match[1])
258		}
259	}
260
261	return mdComponentDevices
262}
263