...

Source file src/github.com/prometheus/procfs/mdstat.go

Documentation: github.com/prometheus/procfs

     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  
    14  package procfs
    15  
    16  import (
    17  	"fmt"
    18  	"os"
    19  	"regexp"
    20  	"strconv"
    21  	"strings"
    22  )
    23  
    24  var (
    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.
    34  type 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
    66  func (fs FS) MDStat() ([]MDStat, error) {
    67  	data, err := os.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("%s: Cannot parse %v: %w", ErrFileParse, 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.
    80  func 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("%s: Expected 3+ lines, got %q", ErrFileParse, 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("%w: Too few lines for md device: %q", ErrFileParse, 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("%s: Cannot parse md device lines: %v: %w", ErrFileParse, active, 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("%s: Cannot parse sync line in md device: %q: %w", ErrFileParse, 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  
   168  func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) {
   169  	statusFields := strings.Fields(statusLine)
   170  	if len(statusFields) < 1 {
   171  		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
   172  	}
   173  
   174  	sizeStr := statusFields[0]
   175  	size, err = strconv.ParseInt(sizeStr, 10, 64)
   176  	if err != nil {
   177  		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
   178  	}
   179  
   180  	if strings.Contains(deviceLine, "raid0") || strings.Contains(deviceLine, "linear") {
   181  		// In the device deviceLine, only disks have a number associated with them in [].
   182  		total = int64(strings.Count(deviceLine, "["))
   183  		return total, total, 0, size, nil
   184  	}
   185  
   186  	if strings.Contains(deviceLine, "inactive") {
   187  		return 0, 0, 0, size, nil
   188  	}
   189  
   190  	matches := statusLineRE.FindStringSubmatch(statusLine)
   191  	if len(matches) != 5 {
   192  		return 0, 0, 0, 0, fmt.Errorf("%s: Could not fild all substring matches %s: %w", ErrFileParse, statusLine, err)
   193  	}
   194  
   195  	total, err = strconv.ParseInt(matches[2], 10, 64)
   196  	if err != nil {
   197  		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
   198  	}
   199  
   200  	active, err = strconv.ParseInt(matches[3], 10, 64)
   201  	if err != nil {
   202  		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected active %d: %w", ErrFileParse, active, err)
   203  	}
   204  	down = int64(strings.Count(matches[4], "_"))
   205  
   206  	return active, total, down, size, nil
   207  }
   208  
   209  func evalRecoveryLine(recoveryLine string) (syncedBlocks int64, pct float64, finish float64, speed float64, err error) {
   210  	matches := recoveryLineBlocksRE.FindStringSubmatch(recoveryLine)
   211  	if len(matches) != 2 {
   212  		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected recoveryLine %s: %w", ErrFileParse, recoveryLine, err)
   213  	}
   214  
   215  	syncedBlocks, err = strconv.ParseInt(matches[1], 10, 64)
   216  	if err != nil {
   217  		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected parsing of recoveryLine %q: %w", ErrFileParse, recoveryLine, err)
   218  	}
   219  
   220  	// Get percentage complete
   221  	matches = recoveryLinePctRE.FindStringSubmatch(recoveryLine)
   222  	if len(matches) != 2 {
   223  		return syncedBlocks, 0, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching percentage %s", ErrFileParse, recoveryLine)
   224  	}
   225  	pct, err = strconv.ParseFloat(strings.TrimSpace(matches[1]), 64)
   226  	if err != nil {
   227  		return syncedBlocks, 0, 0, 0, fmt.Errorf("%w: Error parsing float from recoveryLine %q", ErrFileParse, recoveryLine)
   228  	}
   229  
   230  	// Get time expected left to complete
   231  	matches = recoveryLineFinishRE.FindStringSubmatch(recoveryLine)
   232  	if len(matches) != 2 {
   233  		return syncedBlocks, pct, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching est. finish time: %s", ErrFileParse, recoveryLine)
   234  	}
   235  	finish, err = strconv.ParseFloat(matches[1], 64)
   236  	if err != nil {
   237  		return syncedBlocks, pct, 0, 0, fmt.Errorf("%w: Unable to parse float from recoveryLine: %q", ErrFileParse, recoveryLine)
   238  	}
   239  
   240  	// Get recovery speed
   241  	matches = recoveryLineSpeedRE.FindStringSubmatch(recoveryLine)
   242  	if len(matches) != 2 {
   243  		return syncedBlocks, pct, finish, 0, fmt.Errorf("%w: Unexpected recoveryLine value: %s", ErrFileParse, recoveryLine)
   244  	}
   245  	speed, err = strconv.ParseFloat(matches[1], 64)
   246  	if err != nil {
   247  		return syncedBlocks, pct, finish, 0, fmt.Errorf("%s: Error parsing float from recoveryLine: %q: %w", ErrFileParse, recoveryLine, err)
   248  	}
   249  
   250  	return syncedBlocks, pct, finish, speed, nil
   251  }
   252  
   253  func evalComponentDevices(deviceFields []string) []string {
   254  	mdComponentDevices := make([]string, 0)
   255  	if len(deviceFields) > 3 {
   256  		for _, field := range deviceFields[4:] {
   257  			match := componentDeviceRE.FindStringSubmatch(field)
   258  			if match == nil {
   259  				continue
   260  			}
   261  			mdComponentDevices = append(mdComponentDevices, match[1])
   262  		}
   263  	}
   264  
   265  	return mdComponentDevices
   266  }
   267  

View as plain text