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