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, 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