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	recoveryLineRE    = regexp.MustCompile(`\((\d+)/\d+\)`)
27	componentDeviceRE = regexp.MustCompile(`(.*)\[\d+\]`)
28)
29
30// MDStat holds info parsed from /proc/mdstat.
31type MDStat struct {
32	// Name of the device.
33	Name string
34	// activity-state of the device.
35	ActivityState string
36	// Number of active disks.
37	DisksActive int64
38	// Total number of disks the device requires.
39	DisksTotal int64
40	// Number of failed disks.
41	DisksFailed int64
42	// Spare disks in the device.
43	DisksSpare int64
44	// Number of blocks the device holds.
45	BlocksTotal int64
46	// Number of blocks on the device that are in sync.
47	BlocksSynced int64
48	// Name of md component devices
49	Devices []string
50}
51
52// MDStat parses an mdstat-file (/proc/mdstat) and returns a slice of
53// structs containing the relevant info.  More information available here:
54// https://raid.wiki.kernel.org/index.php/Mdstat
55func (fs FS) MDStat() ([]MDStat, error) {
56	data, err := ioutil.ReadFile(fs.proc.Path("mdstat"))
57	if err != nil {
58		return nil, err
59	}
60	mdstat, err := parseMDStat(data)
61	if err != nil {
62		return nil, fmt.Errorf("error parsing mdstat %q: %w", fs.proc.Path("mdstat"), err)
63	}
64	return mdstat, nil
65}
66
67// parseMDStat parses data from mdstat file (/proc/mdstat) and returns a slice of
68// structs containing the relevant info.
69func parseMDStat(mdStatData []byte) ([]MDStat, error) {
70	mdStats := []MDStat{}
71	lines := strings.Split(string(mdStatData), "\n")
72
73	for i, line := range lines {
74		if strings.TrimSpace(line) == "" || line[0] == ' ' ||
75			strings.HasPrefix(line, "Personalities") ||
76			strings.HasPrefix(line, "unused") {
77			continue
78		}
79
80		deviceFields := strings.Fields(line)
81		if len(deviceFields) < 3 {
82			return nil, fmt.Errorf("not enough fields in mdline (expected at least 3): %s", line)
83		}
84		mdName := deviceFields[0] // mdx
85		state := deviceFields[2]  // active or inactive
86
87		if len(lines) <= i+3 {
88			return nil, fmt.Errorf("error parsing %q: too few lines for md device", mdName)
89		}
90
91		// Failed disks have the suffix (F) & Spare disks have the suffix (S).
92		fail := int64(strings.Count(line, "(F)"))
93		spare := int64(strings.Count(line, "(S)"))
94		active, total, size, err := evalStatusLine(lines[i], lines[i+1])
95
96		if err != nil {
97			return nil, fmt.Errorf("error parsing md device lines: %w", err)
98		}
99
100		syncLineIdx := i + 2
101		if strings.Contains(lines[i+2], "bitmap") { // skip bitmap line
102			syncLineIdx++
103		}
104
105		// If device is syncing at the moment, get the number of currently
106		// synced bytes, otherwise that number equals the size of the device.
107		syncedBlocks := size
108		recovering := strings.Contains(lines[syncLineIdx], "recovery")
109		resyncing := strings.Contains(lines[syncLineIdx], "resync")
110		checking := strings.Contains(lines[syncLineIdx], "check")
111
112		// Append recovery and resyncing state info.
113		if recovering || resyncing || checking {
114			if recovering {
115				state = "recovering"
116			} else if checking {
117				state = "checking"
118			} else {
119				state = "resyncing"
120			}
121
122			// Handle case when resync=PENDING or resync=DELAYED.
123			if strings.Contains(lines[syncLineIdx], "PENDING") ||
124				strings.Contains(lines[syncLineIdx], "DELAYED") {
125				syncedBlocks = 0
126			} else {
127				syncedBlocks, err = evalRecoveryLine(lines[syncLineIdx])
128				if err != nil {
129					return nil, fmt.Errorf("error parsing sync line in md device %q: %w", mdName, err)
130				}
131			}
132		}
133
134		mdStats = append(mdStats, MDStat{
135			Name:          mdName,
136			ActivityState: state,
137			DisksActive:   active,
138			DisksFailed:   fail,
139			DisksSpare:    spare,
140			DisksTotal:    total,
141			BlocksTotal:   size,
142			BlocksSynced:  syncedBlocks,
143			Devices:       evalComponentDevices(deviceFields),
144		})
145	}
146
147	return mdStats, nil
148}
149
150func evalStatusLine(deviceLine, statusLine string) (active, total, size int64, err error) {
151
152	sizeStr := strings.Fields(statusLine)[0]
153	size, err = strconv.ParseInt(sizeStr, 10, 64)
154	if err != nil {
155		return 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
156	}
157
158	if strings.Contains(deviceLine, "raid0") || strings.Contains(deviceLine, "linear") {
159		// In the device deviceLine, only disks have a number associated with them in [].
160		total = int64(strings.Count(deviceLine, "["))
161		return total, total, size, nil
162	}
163
164	if strings.Contains(deviceLine, "inactive") {
165		return 0, 0, size, nil
166	}
167
168	matches := statusLineRE.FindStringSubmatch(statusLine)
169	if len(matches) != 4 {
170		return 0, 0, 0, fmt.Errorf("couldn't find all the substring matches: %s", statusLine)
171	}
172
173	total, err = strconv.ParseInt(matches[2], 10, 64)
174	if err != nil {
175		return 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
176	}
177
178	active, err = strconv.ParseInt(matches[3], 10, 64)
179	if err != nil {
180		return 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
181	}
182
183	return active, total, size, nil
184}
185
186func evalRecoveryLine(recoveryLine string) (syncedBlocks int64, err error) {
187	matches := recoveryLineRE.FindStringSubmatch(recoveryLine)
188	if len(matches) != 2 {
189		return 0, fmt.Errorf("unexpected recoveryLine: %s", recoveryLine)
190	}
191
192	syncedBlocks, err = strconv.ParseInt(matches[1], 10, 64)
193	if err != nil {
194		return 0, fmt.Errorf("error parsing int from recoveryLine %q: %w", recoveryLine, err)
195	}
196
197	return syncedBlocks, nil
198}
199
200func evalComponentDevices(deviceFields []string) []string {
201	mdComponentDevices := make([]string, 0)
202	if len(deviceFields) > 3 {
203		for _, field := range deviceFields[4:] {
204			match := componentDeviceRE.FindStringSubmatch(field)
205			if match == nil {
206				continue
207			}
208			mdComponentDevices = append(mdComponentDevices, match[1])
209		}
210	}
211
212	return mdComponentDevices
213}
214