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