123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- package smart
- /*
- SMART.go
- This script uses the smartctl command to retrieve information about the disk.
- It supports both NVMe and SATA disks on Linux systems only.
- */
- import (
- "bufio"
- "errors"
- "os/exec"
- "strconv"
- "strings"
- "imuslab.com/bokofs/bokofsd/mod/diskinfo"
- )
- // GetDiskType checks if the disk is NVMe or SATA
- func GetDiskType(disk string) (DiskType, error) {
- if !strings.HasPrefix(disk, "/dev/") {
- disk = "/dev/" + disk
- }
- //Make sure the target is a disk
- if !diskinfo.DevicePathIsValidDisk(disk) {
- return DiskType_Unknown, errors.New("disk is not a valid disk")
- }
- //Check if the disk is a NVMe or SATA disk
- if strings.HasPrefix(disk, "/dev/nvme") {
- return DiskType_NVMe, nil
- } else if strings.HasPrefix(disk, "/dev/sd") {
- return DiskType_SATA, nil
- }
- return DiskType_Unknown, errors.New("disk is not NVMe or SATA")
- }
- // GetNVMEInfo retrieves NVMe disk information using smartctl
- func GetNVMEInfo(disk string) (*NVMEInfo, error) {
- if !strings.HasPrefix(disk, "/dev/") {
- disk = "/dev/" + disk
- }
- cmd := exec.Command("smartctl", "-i", disk)
- output, err := cmd.Output()
- if err != nil {
- return nil, err
- }
- scanner := bufio.NewScanner(strings.NewReader(string(output)))
- info := &NVMEInfo{}
- for scanner.Scan() {
- line := scanner.Text()
- if strings.HasPrefix(line, "Model Number:") {
- info.ModelNumber = strings.TrimSpace(strings.TrimPrefix(line, "Model Number:"))
- } else if strings.HasPrefix(line, "Serial Number:") {
- info.SerialNumber = strings.TrimSpace(strings.TrimPrefix(line, "Serial Number:"))
- } else if strings.HasPrefix(line, "Firmware Version:") {
- info.FirmwareVersion = strings.TrimSpace(strings.TrimPrefix(line, "Firmware Version:"))
- } else if strings.HasPrefix(line, "PCI Vendor/Subsystem ID:") {
- info.PCIVendorSubsystemID = strings.TrimSpace(strings.TrimPrefix(line, "PCI Vendor/Subsystem ID:"))
- } else if strings.HasPrefix(line, "IEEE OUI Identifier:") {
- info.IEEEOUIIdentifier = strings.TrimSpace(strings.TrimPrefix(line, "IEEE OUI Identifier:"))
- } else if strings.HasPrefix(line, "Total NVM Capacity:") {
- info.TotalNVMeCapacity = strings.TrimSpace(strings.TrimPrefix(line, "Total NVM Capacity:"))
- } else if strings.HasPrefix(line, "Unallocated NVM Capacity:") {
- info.UnallocatedNVMeCapacity = strings.TrimSpace(strings.TrimPrefix(line, "Unallocated NVM Capacity:"))
- } else if strings.HasPrefix(line, "Controller ID:") {
- info.ControllerID = strings.TrimSpace(strings.TrimPrefix(line, "Controller ID:"))
- } else if strings.HasPrefix(line, "NVMe Version:") {
- info.NVMeVersion = strings.TrimSpace(strings.TrimPrefix(line, "NVMe Version:"))
- } else if strings.HasPrefix(line, "Number of Namespaces:") {
- info.NumberOfNamespaces = strings.TrimSpace(strings.TrimPrefix(line, "Number of Namespaces:"))
- } else if strings.HasPrefix(line, "Namespace 1 Size/Capacity:") {
- info.NamespaceSizeCapacity = strings.TrimSpace(strings.TrimPrefix(line, "Namespace 1 Size/Capacity:"))
- } else if strings.HasPrefix(line, "Namespace 1 Utilization:") {
- info.NamespaceUtilization = strings.TrimSpace(strings.TrimPrefix(line, "Namespace 1 Utilization:"))
- } else if strings.HasPrefix(line, "Namespace 1 Formatted LBA Size:") {
- info.NamespaceFormattedLBASize = strings.TrimSpace(strings.TrimPrefix(line, "Namespace 1 Formatted LBA Size:"))
- } else if strings.HasPrefix(line, "Namespace 1 IEEE EUI-64:") {
- info.NamespaceIEEE_EUI_64 = strings.TrimSpace(strings.TrimPrefix(line, "Namespace 1 IEEE EUI-64:"))
- }
- }
- if err := scanner.Err(); err != nil {
- return nil, err
- }
- return info, nil
- }
- // GetSATADiskInfo retrieves SATA disk information using smartctl
- func GetSATAInfo(disk string) (*SATADiskInfo, error) {
- if !strings.HasPrefix(disk, "/dev/") {
- disk = "/dev/" + disk
- }
- cmd := exec.Command("smartctl", "-i", disk)
- output, err := cmd.Output()
- if err != nil {
- return nil, err
- }
- scanner := bufio.NewScanner(strings.NewReader(string(output)))
- info := &SATADiskInfo{}
- for scanner.Scan() {
- line := scanner.Text()
- if strings.HasPrefix(line, "Model Family:") {
- info.ModelFamily = strings.TrimSpace(strings.TrimPrefix(line, "Model Family:"))
- } else if strings.HasPrefix(line, "Device Model:") {
- info.DeviceModel = strings.TrimSpace(strings.TrimPrefix(line, "Device Model:"))
- } else if strings.HasPrefix(line, "Serial Number:") {
- info.SerialNumber = strings.TrimSpace(strings.TrimPrefix(line, "Serial Number:"))
- } else if strings.HasPrefix(line, "Firmware Version:") {
- info.Firmware = strings.TrimSpace(strings.TrimPrefix(line, "Firmware Version:"))
- } else if strings.HasPrefix(line, "User Capacity:") {
- info.UserCapacity = strings.TrimSpace(strings.TrimPrefix(line, "User Capacity:"))
- } else if strings.HasPrefix(line, "Sector Size:") {
- info.SectorSize = strings.TrimSpace(strings.TrimPrefix(line, "Sector Size:"))
- } else if strings.HasPrefix(line, "Rotation Rate:") {
- info.RotationRate = strings.TrimSpace(strings.TrimPrefix(line, "Rotation Rate:"))
- } else if strings.HasPrefix(line, "Form Factor:") {
- info.FormFactor = strings.TrimSpace(strings.TrimPrefix(line, "Form Factor:"))
- } else if strings.HasPrefix(line, "SMART support is:") {
- info.SmartSupport = strings.TrimSpace(strings.TrimPrefix(line, "SMART support is:")) == "Enabled"
- }
- }
- if err := scanner.Err(); err != nil {
- return nil, err
- }
- return info, nil
- }
- // SetSMARTEnableOnDisk enables or disables SMART on the specified disk
- func SetSMARTEnableOnDisk(disk string, isEnabled bool) error {
- if !strings.HasPrefix(disk, "/dev/") {
- disk = "/dev/" + disk
- }
- enableCmd := "off"
- if isEnabled {
- enableCmd = "on"
- }
- cmd := exec.Command("smartctl", "-s", enableCmd, disk)
- output, err := cmd.Output()
- if err != nil {
- return err
- }
- if strings.Contains(string(output), "SMART Enabled") {
- return nil
- } else {
- // Print the command output to STDOUT if enabling SMART failed
- println(string(output))
- return errors.New("failed to enable SMART on disk")
- }
- }
- // GetDiskSMARTCheck retrieves the SMART health status of the specified disk
- // Usually only returns "PASSED" or "FAILED"
- func GetDiskSMARTCheck(diskname string) (*SMARTTestResult, error) {
- if !strings.HasPrefix(diskname, "/dev/") {
- diskname = "/dev/" + diskname
- }
- cmd := exec.Command("smartctl", "-H", "-A", diskname)
- output, err := cmd.Output()
- if err != nil {
- // Check if the error is due to exit code 32 (non-critical error for some disks)
- if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 32 {
- // Ignore the error and proceed
- } else {
- // Print the command output to STDOUT if the command fails
- println(string(output))
- return nil, err
- }
- }
- scanner := bufio.NewScanner(strings.NewReader(string(output)))
- result := &SMARTTestResult{
- TestResult: "Unknown",
- MarginalAttributes: make([]SMARTAttribute, 0),
- }
- var inAttributesSection bool = false
- for scanner.Scan() {
- line := scanner.Text()
- //fmt.Println(line)
- // Check for overall health result
- if strings.HasPrefix(line, "SMART overall-health self-assessment test result:") {
- result.TestResult = strings.TrimSpace(strings.TrimPrefix(line, "SMART overall-health self-assessment test result:"))
- }
- // Detect the start of the attributes section
- if strings.HasPrefix(line, "ID# ATTRIBUTE_NAME") {
- inAttributesSection = true
- continue
- }
- // Parse marginal attributes
- if inAttributesSection {
- fields := strings.Fields(line)
- if len(fields) >= 10 {
- id, err := strconv.Atoi(fields[0])
- if err != nil {
- continue
- }
- value, err := strconv.Atoi(fields[3])
- if err != nil {
- continue
- }
- worst, err := strconv.Atoi(fields[4])
- if err != nil {
- continue
- }
- threshold, err := strconv.Atoi(fields[5])
- if err != nil {
- continue
- }
- attribute := SMARTAttribute{
- ID: id,
- Name: fields[1],
- Flag: fields[2],
- Value: value,
- Worst: worst,
- Threshold: threshold,
- Type: fields[6],
- Updated: fields[7],
- WhenFailed: fields[8],
- RawValue: strings.Join(fields[9:], " "),
- }
- result.MarginalAttributes = append(result.MarginalAttributes, attribute)
- }
- }
- }
- if err := scanner.Err(); err != nil {
- // Print the command output to STDOUT if parsing failed
- println(string(output))
- return nil, err
- }
- if result.TestResult == "" {
- return nil, errors.New("unable to determine SMART health status")
- }
- return result, nil
- }
- func GetDiskSMARTHealthSummary(diskname string) (*DriveHealthInfo, error) {
- smartCheck, err := GetDiskSMARTCheck(diskname)
- if err != nil {
- return nil, err
- }
- healthInfo := &DriveHealthInfo{
- DeviceName: diskname,
- IsHealthy: strings.ToUpper(smartCheck.TestResult) == "PASSED",
- }
- //Populate the device model and serial number from SMARTInfo
- dt, err := GetDiskType(diskname)
- if err != nil {
- return nil, err
- }
- if dt == DiskType_SATA {
- sataInfo, err := GetSATAInfo(diskname)
- if err != nil {
- return nil, err
- }
- healthInfo.DeviceModel = sataInfo.DeviceModel
- healthInfo.SerialNumber = sataInfo.SerialNumber
- healthInfo.IsSSD = strings.Contains(sataInfo.RotationRate, "Solid State")
- } else if dt == DiskType_NVMe {
- nvmeInfo, err := GetNVMEInfo(diskname)
- if err != nil {
- return nil, err
- }
- healthInfo.DeviceModel = nvmeInfo.ModelNumber
- healthInfo.SerialNumber = nvmeInfo.SerialNumber
- healthInfo.IsNVMe = true
- } else {
- return nil, errors.New("unsupported disk type")
- }
- for _, attr := range smartCheck.MarginalAttributes {
- switch attr.Name {
- case "Power_On_Hours":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.PowerOnHours = value
- }
- case "Power_Cycle_Count":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.PowerCycleCount = value
- }
- case "Reallocated_Sector_Ct":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.ReallocatedSectors = value
- }
- case "Wear_Leveling_Count":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.WearLevelingCount = value
- }
- case "Uncorrectable_Error_Cnt":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.UncorrectableErrors = value
- }
- case "Current_Pending_Sector":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.PendingSectors = value
- }
- case "ECC_Recovered":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.ECCRecovered = value
- }
- case "UDMA_CRC_Error_Count":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.UDMACRCErrors = value
- }
- case "Total_LBAs_Written":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.TotalLBAWritten = value
- }
- case "Total_LBAs_Read":
- if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
- healthInfo.TotalLBARead = value
- }
- }
- }
- return healthInfo, nil
- }
|