smart.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. package smart
  2. /*
  3. SMART.go
  4. This script uses the smartctl command to retrieve information about the disk.
  5. It supports both NVMe and SATA disks on Linux systems only.
  6. */
  7. import (
  8. "bufio"
  9. "errors"
  10. "os/exec"
  11. "strconv"
  12. "strings"
  13. "imuslab.com/bokofs/bokofsd/mod/diskinfo"
  14. )
  15. // GetDiskType checks if the disk is NVMe or SATA
  16. func GetDiskType(disk string) (DiskType, error) {
  17. if !strings.HasPrefix(disk, "/dev/") {
  18. disk = "/dev/" + disk
  19. }
  20. //Make sure the target is a disk
  21. if !diskinfo.DevicePathIsValidDisk(disk) {
  22. return DiskType_Unknown, errors.New("disk is not a valid disk")
  23. }
  24. //Check if the disk is a NVMe or SATA disk
  25. if strings.HasPrefix(disk, "/dev/nvme") {
  26. return DiskType_NVMe, nil
  27. } else if strings.HasPrefix(disk, "/dev/sd") {
  28. return DiskType_SATA, nil
  29. }
  30. return DiskType_Unknown, errors.New("disk is not NVMe or SATA")
  31. }
  32. // GetNVMEInfo retrieves NVMe disk information using smartctl
  33. func GetNVMEInfo(disk string) (*NVMEInfo, error) {
  34. if !strings.HasPrefix(disk, "/dev/") {
  35. disk = "/dev/" + disk
  36. }
  37. cmd := exec.Command("smartctl", "-i", disk)
  38. output, err := cmd.Output()
  39. if err != nil {
  40. return nil, err
  41. }
  42. scanner := bufio.NewScanner(strings.NewReader(string(output)))
  43. info := &NVMEInfo{}
  44. for scanner.Scan() {
  45. line := scanner.Text()
  46. if strings.HasPrefix(line, "Model Number:") {
  47. info.ModelNumber = strings.TrimSpace(strings.TrimPrefix(line, "Model Number:"))
  48. } else if strings.HasPrefix(line, "Serial Number:") {
  49. info.SerialNumber = strings.TrimSpace(strings.TrimPrefix(line, "Serial Number:"))
  50. } else if strings.HasPrefix(line, "Firmware Version:") {
  51. info.FirmwareVersion = strings.TrimSpace(strings.TrimPrefix(line, "Firmware Version:"))
  52. } else if strings.HasPrefix(line, "PCI Vendor/Subsystem ID:") {
  53. info.PCIVendorSubsystemID = strings.TrimSpace(strings.TrimPrefix(line, "PCI Vendor/Subsystem ID:"))
  54. } else if strings.HasPrefix(line, "IEEE OUI Identifier:") {
  55. info.IEEEOUIIdentifier = strings.TrimSpace(strings.TrimPrefix(line, "IEEE OUI Identifier:"))
  56. } else if strings.HasPrefix(line, "Total NVM Capacity:") {
  57. info.TotalNVMeCapacity = strings.TrimSpace(strings.TrimPrefix(line, "Total NVM Capacity:"))
  58. } else if strings.HasPrefix(line, "Unallocated NVM Capacity:") {
  59. info.UnallocatedNVMeCapacity = strings.TrimSpace(strings.TrimPrefix(line, "Unallocated NVM Capacity:"))
  60. } else if strings.HasPrefix(line, "Controller ID:") {
  61. info.ControllerID = strings.TrimSpace(strings.TrimPrefix(line, "Controller ID:"))
  62. } else if strings.HasPrefix(line, "NVMe Version:") {
  63. info.NVMeVersion = strings.TrimSpace(strings.TrimPrefix(line, "NVMe Version:"))
  64. } else if strings.HasPrefix(line, "Number of Namespaces:") {
  65. info.NumberOfNamespaces = strings.TrimSpace(strings.TrimPrefix(line, "Number of Namespaces:"))
  66. } else if strings.HasPrefix(line, "Namespace 1 Size/Capacity:") {
  67. info.NamespaceSizeCapacity = strings.TrimSpace(strings.TrimPrefix(line, "Namespace 1 Size/Capacity:"))
  68. } else if strings.HasPrefix(line, "Namespace 1 Utilization:") {
  69. info.NamespaceUtilization = strings.TrimSpace(strings.TrimPrefix(line, "Namespace 1 Utilization:"))
  70. } else if strings.HasPrefix(line, "Namespace 1 Formatted LBA Size:") {
  71. info.NamespaceFormattedLBASize = strings.TrimSpace(strings.TrimPrefix(line, "Namespace 1 Formatted LBA Size:"))
  72. } else if strings.HasPrefix(line, "Namespace 1 IEEE EUI-64:") {
  73. info.NamespaceIEEE_EUI_64 = strings.TrimSpace(strings.TrimPrefix(line, "Namespace 1 IEEE EUI-64:"))
  74. }
  75. }
  76. if err := scanner.Err(); err != nil {
  77. return nil, err
  78. }
  79. return info, nil
  80. }
  81. // GetSATADiskInfo retrieves SATA disk information using smartctl
  82. func GetSATAInfo(disk string) (*SATADiskInfo, error) {
  83. if !strings.HasPrefix(disk, "/dev/") {
  84. disk = "/dev/" + disk
  85. }
  86. cmd := exec.Command("smartctl", "-i", disk)
  87. output, err := cmd.Output()
  88. if err != nil {
  89. return nil, err
  90. }
  91. scanner := bufio.NewScanner(strings.NewReader(string(output)))
  92. info := &SATADiskInfo{}
  93. for scanner.Scan() {
  94. line := scanner.Text()
  95. if strings.HasPrefix(line, "Model Family:") {
  96. info.ModelFamily = strings.TrimSpace(strings.TrimPrefix(line, "Model Family:"))
  97. } else if strings.HasPrefix(line, "Device Model:") {
  98. info.DeviceModel = strings.TrimSpace(strings.TrimPrefix(line, "Device Model:"))
  99. } else if strings.HasPrefix(line, "Serial Number:") {
  100. info.SerialNumber = strings.TrimSpace(strings.TrimPrefix(line, "Serial Number:"))
  101. } else if strings.HasPrefix(line, "Firmware Version:") {
  102. info.Firmware = strings.TrimSpace(strings.TrimPrefix(line, "Firmware Version:"))
  103. } else if strings.HasPrefix(line, "User Capacity:") {
  104. info.UserCapacity = strings.TrimSpace(strings.TrimPrefix(line, "User Capacity:"))
  105. } else if strings.HasPrefix(line, "Sector Size:") {
  106. info.SectorSize = strings.TrimSpace(strings.TrimPrefix(line, "Sector Size:"))
  107. } else if strings.HasPrefix(line, "Rotation Rate:") {
  108. info.RotationRate = strings.TrimSpace(strings.TrimPrefix(line, "Rotation Rate:"))
  109. } else if strings.HasPrefix(line, "Form Factor:") {
  110. info.FormFactor = strings.TrimSpace(strings.TrimPrefix(line, "Form Factor:"))
  111. } else if strings.HasPrefix(line, "SMART support is:") {
  112. info.SmartSupport = strings.TrimSpace(strings.TrimPrefix(line, "SMART support is:")) == "Enabled"
  113. }
  114. }
  115. if err := scanner.Err(); err != nil {
  116. return nil, err
  117. }
  118. return info, nil
  119. }
  120. // SetSMARTEnableOnDisk enables or disables SMART on the specified disk
  121. func SetSMARTEnableOnDisk(disk string, isEnabled bool) error {
  122. if !strings.HasPrefix(disk, "/dev/") {
  123. disk = "/dev/" + disk
  124. }
  125. enableCmd := "off"
  126. if isEnabled {
  127. enableCmd = "on"
  128. }
  129. cmd := exec.Command("smartctl", "-s", enableCmd, disk)
  130. output, err := cmd.Output()
  131. if err != nil {
  132. return err
  133. }
  134. if strings.Contains(string(output), "SMART Enabled") {
  135. return nil
  136. } else {
  137. // Print the command output to STDOUT if enabling SMART failed
  138. println(string(output))
  139. return errors.New("failed to enable SMART on disk")
  140. }
  141. }
  142. // GetDiskSMARTCheck retrieves the SMART health status of the specified disk
  143. // Usually only returns "PASSED" or "FAILED"
  144. func GetDiskSMARTCheck(diskname string) (*SMARTTestResult, error) {
  145. if !strings.HasPrefix(diskname, "/dev/") {
  146. diskname = "/dev/" + diskname
  147. }
  148. cmd := exec.Command("smartctl", "-H", "-A", diskname)
  149. output, err := cmd.Output()
  150. if err != nil {
  151. // Check if the error is due to exit code 32 (non-critical error for some disks)
  152. if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 32 {
  153. // Ignore the error and proceed
  154. } else {
  155. // Print the command output to STDOUT if the command fails
  156. println(string(output))
  157. return nil, err
  158. }
  159. }
  160. scanner := bufio.NewScanner(strings.NewReader(string(output)))
  161. result := &SMARTTestResult{
  162. TestResult: "Unknown",
  163. MarginalAttributes: make([]SMARTAttribute, 0),
  164. }
  165. var inAttributesSection bool = false
  166. for scanner.Scan() {
  167. line := scanner.Text()
  168. //fmt.Println(line)
  169. // Check for overall health result
  170. if strings.HasPrefix(line, "SMART overall-health self-assessment test result:") {
  171. result.TestResult = strings.TrimSpace(strings.TrimPrefix(line, "SMART overall-health self-assessment test result:"))
  172. }
  173. // Detect the start of the attributes section
  174. if strings.HasPrefix(line, "ID# ATTRIBUTE_NAME") {
  175. inAttributesSection = true
  176. continue
  177. }
  178. // Parse marginal attributes
  179. if inAttributesSection {
  180. fields := strings.Fields(line)
  181. if len(fields) >= 10 {
  182. id, err := strconv.Atoi(fields[0])
  183. if err != nil {
  184. continue
  185. }
  186. value, err := strconv.Atoi(fields[3])
  187. if err != nil {
  188. continue
  189. }
  190. worst, err := strconv.Atoi(fields[4])
  191. if err != nil {
  192. continue
  193. }
  194. threshold, err := strconv.Atoi(fields[5])
  195. if err != nil {
  196. continue
  197. }
  198. attribute := SMARTAttribute{
  199. ID: id,
  200. Name: fields[1],
  201. Flag: fields[2],
  202. Value: value,
  203. Worst: worst,
  204. Threshold: threshold,
  205. Type: fields[6],
  206. Updated: fields[7],
  207. WhenFailed: fields[8],
  208. RawValue: strings.Join(fields[9:], " "),
  209. }
  210. result.MarginalAttributes = append(result.MarginalAttributes, attribute)
  211. }
  212. }
  213. }
  214. if err := scanner.Err(); err != nil {
  215. // Print the command output to STDOUT if parsing failed
  216. println(string(output))
  217. return nil, err
  218. }
  219. if result.TestResult == "" {
  220. return nil, errors.New("unable to determine SMART health status")
  221. }
  222. return result, nil
  223. }
  224. func GetDiskSMARTHealthSummary(diskname string) (*DriveHealthInfo, error) {
  225. smartCheck, err := GetDiskSMARTCheck(diskname)
  226. if err != nil {
  227. return nil, err
  228. }
  229. healthInfo := &DriveHealthInfo{
  230. DeviceName: diskname,
  231. IsHealthy: strings.ToUpper(smartCheck.TestResult) == "PASSED",
  232. }
  233. //Populate the device model and serial number from SMARTInfo
  234. dt, err := GetDiskType(diskname)
  235. if err != nil {
  236. return nil, err
  237. }
  238. if dt == DiskType_SATA {
  239. sataInfo, err := GetSATAInfo(diskname)
  240. if err != nil {
  241. return nil, err
  242. }
  243. healthInfo.DeviceModel = sataInfo.DeviceModel
  244. healthInfo.SerialNumber = sataInfo.SerialNumber
  245. healthInfo.IsSSD = strings.Contains(sataInfo.RotationRate, "Solid State")
  246. } else if dt == DiskType_NVMe {
  247. nvmeInfo, err := GetNVMEInfo(diskname)
  248. if err != nil {
  249. return nil, err
  250. }
  251. healthInfo.DeviceModel = nvmeInfo.ModelNumber
  252. healthInfo.SerialNumber = nvmeInfo.SerialNumber
  253. healthInfo.IsNVMe = true
  254. } else {
  255. return nil, errors.New("unsupported disk type")
  256. }
  257. for _, attr := range smartCheck.MarginalAttributes {
  258. switch attr.Name {
  259. case "Power_On_Hours":
  260. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  261. healthInfo.PowerOnHours = value
  262. }
  263. case "Power_Cycle_Count":
  264. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  265. healthInfo.PowerCycleCount = value
  266. }
  267. case "Reallocated_Sector_Ct":
  268. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  269. healthInfo.ReallocatedSectors = value
  270. }
  271. case "Wear_Leveling_Count":
  272. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  273. healthInfo.WearLevelingCount = value
  274. }
  275. case "Uncorrectable_Error_Cnt":
  276. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  277. healthInfo.UncorrectableErrors = value
  278. }
  279. case "Current_Pending_Sector":
  280. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  281. healthInfo.PendingSectors = value
  282. }
  283. case "ECC_Recovered":
  284. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  285. healthInfo.ECCRecovered = value
  286. }
  287. case "UDMA_CRC_Error_Count":
  288. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  289. healthInfo.UDMACRCErrors = value
  290. }
  291. case "Total_LBAs_Written":
  292. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  293. healthInfo.TotalLBAWritten = value
  294. }
  295. case "Total_LBAs_Read":
  296. if value, err := strconv.ParseUint(attr.RawValue, 10, 64); err == nil {
  297. healthInfo.TotalLBARead = value
  298. }
  299. }
  300. }
  301. return healthInfo, nil
  302. }