blob: 8aef3d8fd0086984dbcad03b5a2d396591b0070b [file]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package storage reports information retrieved from storage-info-common.sh on behalf of tests.
package storage
import (
"bytes"
"context"
"regexp"
"strconv"
"strings"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
// Type stands for various Chromebook storage devices.
type Type int
// LifeStatus stands for a simplified overview of device health.
type LifeStatus int
const (
// EMMC (Embedded Multi-Media Controller) devices are a single package flash storage and controller.
EMMC Type = iota
// NVMe (Non-Volatile Memory Express) interface. PCIe cards, but more commonly M.2 in Chromebooks.
NVMe
// SSD (Solid State Drive) devices connected through a SATA interface.
SSD
)
const (
// Healthy means that the device does not indicate failure or limited remaining life time.
Healthy LifeStatus = iota
// Failing indicates the storage device failed or will soon.
Failing
)
// Info contains information about a storage device.
type Info struct {
// Name of the storage device.
Name string
// Device contains the underlying hardware device type.
Device Type
// Failing contains a final assessment that the device failed or will fail soon.
Status LifeStatus
// PercentageUsed contains the percentage of SSD life that has been used.
// For NVMe and SATA devices, an exact value is returned. For eMMC devices,
// the value is reported in 10's of percents (10, 20, 30, etc.).
// In case of any error reading SSD usage data, value will be -1.
PercentageUsed int64
// TotalBytesWritten corresponds to total amount of data (in bytes) written
// to the disk.
TotalBytesWritten int64
}
// Get runs the storage info shell script and returns its info.
func Get(ctx context.Context) (*Info, error) {
cmd := testexec.CommandContext(ctx, "sh", "-c", ". /usr/share/misc/storage-info-common.sh; get_storage_info")
out, err := cmd.Output(testexec.DumpLogOnError)
if err != nil {
return nil, errors.Wrap(err, "failed to run storage info command")
}
return parseGetStorageInfoOutput(ctx, out)
}
// parseGetStorageInfoOutput parses the storage information to find the device type and life status.
func parseGetStorageInfoOutput(ctx context.Context, out []byte) (*Info, error) {
out = bytes.TrimSpace(out)
if len(out) == 0 {
return nil, errors.New("get storage info did not produce output")
}
lines := strings.Split(string(out), "\n")
deviceType, err := parseDeviceType(lines)
if err != nil {
return nil, errors.Wrap(err, "failed to parse storage info for device type")
}
var lifeStatus LifeStatus
var percentageUsed, bytesWritten int64
var name string
switch deviceType {
case EMMC:
lifeStatus, err = parseDeviceHealtheMMC(lines)
if err != nil {
return nil, errors.Wrap(err, "failed to parse eMMC health")
}
name = parseDeviceNameEMMC(lines)
percentageUsed, err = parsePercentageUsedEMMC(lines, emmcUsedA, emmcUsedB)
if err != nil {
testing.ContextLog(ctx, "Error acquiring usage of eMMC device: ", name, err)
}
case NVMe:
lifeStatus, err = parseDeviceHealthNVMe(lines)
if err != nil {
return nil, errors.Wrap(err, "failed to parse NVMe health")
}
name = parseDeviceNameNVMe(lines)
bytesWritten, err = parseTotalBytesWrittenNVMe(lines)
if err != nil {
testing.ContextLog(ctx, "Error acquiring TBW of NVMe device: ", name, err)
}
percentageUsed, err = parsePercentageUsed(lines, nvmeUsed)
if err != nil {
testing.ContextLog(ctx, "Error acquiring usage of NVMe device: ", name, err)
}
case SSD:
lifeStatus, err = parseDeviceHealthSSD(lines)
if err != nil {
return nil, errors.Wrap(err, "failed to parse SSD health")
}
name = parseDeviceNameSATA(lines)
bytesWritten, err = parseTotalBytesWrittenSATA(lines)
if err != nil {
testing.ContextLog(ctx, "Error acquiring TBW of SSD device: ", name, err)
}
percentageUsed, err = parsePercentageUsed(lines, ssdUsed)
if err != nil {
testing.ContextLog(ctx, "Error acquiring usage of SSD device: ", name, err)
}
default:
return nil, errors.Errorf("parsing device health for type %v is not supported", deviceType)
}
return &Info{Name: name, Device: deviceType, Status: lifeStatus,
PercentageUsed: percentageUsed, TotalBytesWritten: bytesWritten}, nil
}
var (
// nameDetectEMMC detects the name of a eMMC-based device using a regex.
nameDetectEMMC = regexp.MustCompile(`\s*name\s+\|\s(?P<param>\S+).*`)
// nameDetectNVMeSATA detects the name of a NVMe and SATA-based device using a regex.
nameDetectNVMeSATA = regexp.MustCompile(`\s*Serial Number:\s+(?P<param>\S+).*`)
// nvmeDetect detects if storage device is NVME using a regex.
// Example NVMe SMART text: " SMART/Health Information (NVMe Log 0x02, NSID 0xffffffff)"
nvmeDetect = regexp.MustCompile(`\s*SMART.*NVMe Log`)
// ssdDetect detects if storage device is SSD using a regex.
// Example SSD ATA text, " ATA Version is: ACS-2 T13/2015-D revision 3"
ssdDetect = regexp.MustCompile(`\s*ATA Version`)
// emmcDetect detects if storage device is eMMC using a regex.
// Example eMMC CSD text, " Extended CSD rev 1.8 (MMC 5.1)"
emmcDetect = regexp.MustCompile(`\s*Extended CSD rev.*MMC`)
// emmcVersion finds eMMC version of device using a regex.
// Example eMMC CSD text, " Extended CSD rev 1.8 (MMC 5.1)".
emmcVersion = regexp.MustCompile(`\s*Extended CSD rev.*MMC (?P<version>\d+.\d+)`)
// emmcFailing detects if eMMC device is failing using a regex.
// Example CSD text containing Pre EOL information. 0x03 means Urgent.
// "Pre EOL information [PRE_EOL_INFO: 0x03]"
// i.e. Urgent
// We want to detect 0x03 for the Urgent case.
// That indicates that the eMMC is near the end of life.
emmcFailing = regexp.MustCompile(`.*(?P<param>PRE_EOL_INFO]?: 0x03)`)
// emmcUsedA detects the Lifetime Estimation type A value for the eMMC drive.
// Example eMMC usage text:
// "Device life time estimation type A [DEVICE_LIFE_TIME_EST_TYP_A: 0x01]"
emmcUsedA = regexp.MustCompile(`\s*.*DEVICE_LIFE_TIME_EST_TYP_A]?: 0x(?P<lifetime>0\S)`)
// emmcUsedB detects the Lifetime Estimation type A value for the eMMC drive.
// Example eMMC usage text:
// "Device life time estimation type B [DEVICE_LIFE_TIME_EST_TYP_B: 0x01]"
emmcUsedB = regexp.MustCompile(`\s*.*DEVICE_LIFE_TIME_EST_TYP_B]?: 0x(?P<lifetime>0\S)`)
// nvmeUsed detects the usage (in percents) of the NVMe drive.
// Example NVMe usage text: " Percentage Used: 0%"
nvmeUsed = regexp.MustCompile(`\s*Percentage Used:\s*(?P<percentage>\d*)`)
// nvmeSpare and nvmeThreshold are used to detect if nvme is failing using regex.
// If Available Spare is less than Available Spare Threshold, the device
// is likely close to failing and we should remove the DUT.
// Example NVMe usage text: " Available Spare: 100%"
// "Available Spare Threshold: 10%"
nvmeSpare = regexp.MustCompile(`\s*Available Spare:\s+(?P<spare>\d+)%`)
nvmeThreshold = regexp.MustCompile(`\s*Available Spare Threshold:\s+(?P<thresh>\d+)%`)
// ssdUsed detects the usage of ssd device.
// Example SSD usage text: "0x07 0x008 1 91 --- Percentage Used Endurance Indicator"
ssdUsed = regexp.MustCompile(`.*\s{3,}(?P<percentage>\d*).*Percentage Used Endurance Indicator`)
// ssdFailingLegacy detects if ssd device is failing using a regex.
// The indicator used here is not reported for all SATA devices.
ssdFailingLegacy = regexp.MustCompile(`\s*(?P<param>\S+\s\S+)` + // ID and attribute name
`\s+[P-][O-][S-][R-][C-][K-]` + // Flags
`(\s+\d{3}){3}` + // Three 3-digit numbers
`\s+NOW`) // Fail indicator
// ssdFailing detects if ssd device is failing using a regex.
// nvmeUnitsWritten is the regexp for matching TBW value for NVMe devices.
nvmeUnitsWritten = regexp.MustCompile(`\s*Data Units Written:\s*(?P<param>\d+[,\d]*)`)
// ssdUnitsWritten is the regexp for matching TBW value for SATA SSD devices.
ssdUnitsWritten = regexp.MustCompile(`.*Total_LBAs_Written.*\s+(?P<param>\d+)$`)
// We look for non-zero values for either attribute 160 Uncorrectable_Error_Cnt
// or attribute 187 Reported_Uncorrect.
// Example usage text: "187 Reported_Uncorrect -O---- 100 100 000 - 0"
ssdFailing = regexp.MustCompile(`\s*(?P<param>(160\s+Uncorrectable_Error_Cnt|` +
`187\s+Reported_Uncorrect))` + // ID and attribute name
`\s+[P-][O-][S-][R-][C-][K-]` + // Flags
`(\s+\d{1,3}){3}` + // Three 1 to 3-digit numbers
`\s+(NOW|-)` + // Fail indicator
`\s+(?P<value>[1-9][0-9]*)`) // Non-zero raw value
)
// parseDeviceType searches outlines for storage device type.
func parseDeviceType(outLines []string) (Type, error) {
for _, line := range outLines {
if nvmeDetect.MatchString(line) {
return NVMe, nil
}
if ssdDetect.MatchString(line) {
return SSD, nil
}
if emmcDetect.MatchString(line) {
return EMMC, nil
}
}
return 0, errors.New("failed to detect a device type")
}
// parseDeviceNameEMMC searches outlines for eMMC-based storage device name.
func parseDeviceNameEMMC(outLines []string) string {
for _, line := range outLines {
match := nameDetectEMMC.FindStringSubmatch(line)
if match != nil {
return match[1]
}
}
return "EMMC"
}
// parseDeviceNameNVMe searches outlines for NVMe-based storage device name.
func parseDeviceNameNVMe(outLines []string) string {
for _, line := range outLines {
match := nameDetectNVMeSATA.FindStringSubmatch(line)
if match != nil {
return match[1]
}
}
return "NVME"
}
// parseDeviceNameSATA searches outlines for SATA-based SSD storage device name.
func parseDeviceNameSATA(outLines []string) string {
for _, line := range outLines {
match := nameDetectNVMeSATA.FindStringSubmatch(line)
if match != nil {
return match[1]
}
}
return "SATA"
}
// parseDeviceHealtheMMC analyzes eMMC for indications of failure. For additional information,
// refer to JEDEC standard 84-B50 which describes the extended CSD register. In this case,
// we focus on the PRE_EOL_INFO register.
func parseDeviceHealtheMMC(outLines []string) (LifeStatus, error) {
// Device life estimates were introduced in version 5.0
const emmcMinimumVersion = 5.0
for _, line := range outLines {
match := emmcVersion.FindStringSubmatch(line)
if match == nil {
continue
}
version, err := strconv.ParseFloat(match[1], 64)
if err != nil {
return 0, errors.Errorf("failed to parse eMMC version %v", match[1])
}
if version < emmcMinimumVersion {
return 0, errors.Errorf("eMMC version %v less than %v", version, emmcMinimumVersion)
}
}
for _, line := range outLines {
if emmcFailing.MatchString(line) {
return Failing, nil
}
}
return Healthy, nil
}
// parseDeviceHealthNVMe analyzes NVMe SMART attributes for indications of failure.
// Returns usage percentage, drive health status and error (if encountered).
func parseDeviceHealthNVMe(outLines []string) (LifeStatus, error) {
// Flag devices which report available spare less than available threshold
for i, line := range outLines {
match := nvmeSpare.FindStringSubmatch(line)
if match == nil {
continue
}
tmatch := nvmeThreshold.FindStringSubmatch(outLines[i+1])
if tmatch == nil {
return 0, errors.Errorf("failed to find available spare threshold %v", match[1])
}
sparePercent, err := strconv.ParseInt(match[1], 10, 32)
if err != nil {
return 0, errors.Errorf("failed to parse available spare %v", match[1])
}
threshPercent, err := strconv.ParseInt(tmatch[1], 10, 32)
if err != nil {
return 0, errors.Errorf("failed to parse available spare threshold %v", tmatch[1])
}
if sparePercent < threshPercent {
return Failing, nil
}
}
return Healthy, nil
}
// parseDeviceHealthSSD analyzes storage information for indications of failure specific to SSDs.
// Returns usage percentage, drive health status and error (if encountered).
func parseDeviceHealthSSD(outLines []string) (LifeStatus, error) {
// Flag devices which report non-zero uncorrectable errors or that report failing
// End-to-End_Error attribute
for _, line := range outLines {
if ssdFailingLegacy.MatchString(line) {
return Failing, nil
}
match := ssdFailing.FindStringSubmatch(line)
if match == nil {
continue
}
return Failing, nil
}
return Healthy, nil
}
// parsePercentageUsed is a helper function that analyzes the percentage used
// value for extracting disk usage.
func parsePercentageUsed(outLines []string, pattern *regexp.Regexp) (int64, error) {
for _, line := range outLines {
if match := pattern.FindStringSubmatch(line); match != nil {
return strconv.ParseInt(match[1], 10, 32)
}
}
return -1, nil
}
// parsePercentageUsedEMMC is a helper function that analyzes the lifetime estimation
// value for extracting disk usage. eMMC devices report two values for lifetime
// estimates, type A and type B, these values are determined by the vendor. These values
// also represent percentage ranges, for example, 0x01 indicates the device is
// 0% - 10% device life time used. To simplify our charts, we will just use the larger
// of the type A and type B values and convert the bucket value to an equivalent percentage which
// falls in the middle of the bucket the value represents. Each bucket represents a 10% range
// and we will take the median value of that range.
// So, the value 0x01 will be converted to 5% and the value 0x03 will be converted to 25%.
func parsePercentageUsedEMMC(outLines []string, patternA, patternB *regexp.Regexp) (int64, error) {
var typeA, typeB, bucket int64
var err error
for _, line := range outLines {
if match := patternA.FindStringSubmatch(line); match != nil {
bucket, err = strconv.ParseInt(match[1], 16, 64)
typeA = (bucket * 10) - 5
}
if match := patternB.FindStringSubmatch(line); match != nil {
bucket, err = strconv.ParseInt(match[1], 16, 64)
typeB = (bucket * 10) - 5
}
}
if typeA != 0 || typeB != 0 {
if typeA < typeB {
return typeB, err
}
return typeA, err
}
return -1, err
}
// parseTotalBytesWrittenNVMe parses NVMe SMART attribute value to extract
// and return Total Bytes Written data.
func parseTotalBytesWrittenNVMe(lines []string) (int64, error) {
for _, line := range lines {
match := nvmeUnitsWritten.FindStringSubmatch(line)
if match == nil {
continue
}
unitsWritten, err := strconv.ParseInt(strings.ReplaceAll(match[1], ",", ""), 10, 64)
if err != nil {
return 0, errors.Errorf("failed to parse total bytes written %v", match[1])
}
// smartctl reports units written in 1000's of blocks (512 bytes each).
return unitsWritten * 512 * 1000, nil
}
return 0, nil
}
// parseTotalBytesWrittenSATA parses SATA SMART attribute value to extract
// and return Total Bytes Written data.
func parseTotalBytesWrittenSATA(lines []string) (int64, error) {
for _, line := range lines {
match := ssdUnitsWritten.FindStringSubmatch(line)
if match == nil {
continue
}
blocksWritten, err := strconv.ParseInt(match[1], 10, 64)
if err != nil {
return 0, errors.Errorf("failed to parse total bytes written %v", match[1])
}
return blocksWritten * 512, nil
}
return 0, nil
}