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