S.M.A.R.T. drive checking has never been easier
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

252 lines
9.4 KiB

  1. #!/usr/bin/env python3
  2. from re import findall, M, search
  3. from subprocess import Popen, PIPE, STDOUT
  4. from sys import argv
  5. from pprint import pprint
  6. class Device(object):
  7. def __init__(self, path, timeout=3):
  8. self.path = path
  9. self.name = path.split('/')[-1]
  10. self.info = {}
  11. self.timeout = float(timeout)
  12. self.smart_data = {}
  13. self.smart_info = {}
  14. self.smart_health = 'UNKNOWN'
  15. self.smart_support = 'UNKNOWN'
  16. self.vendor_model = ''
  17. def fetch_smart(self):
  18. self.smart_data = {}
  19. self.smart_info = {}
  20. self.smart_health = 'UNKNOWN'
  21. self.smart_support = 'UNKNOWN'
  22. self.vendor_model = ''
  23. process = Popen(['smartctl', '-a', self.path], stdout=PIPE, stderr=STDOUT)
  24. if self.timeout:
  25. returncode = process.wait(self.timeout)
  26. else:
  27. returncode = process.wait()
  28. output = process.stdout.read().decode()
  29. # if returncode != 0: # TODO: IMPROVE ERROR HANDLING!
  30. # raise FileNotFoundError(self.path)
  31. try:
  32. rex_support = search('SMART support is:\s*(Enabled|Disabled)', output)
  33. if rex_support:
  34. self.smart_support = rex_support.group(1)
  35. if 'Unknown USB bridge' in output:
  36. self.smart_health = 'USBB'
  37. if self.name.startswith('nvme'):
  38. info_data, smart_data = output.split('=== START OF SMART DATA SECTION ===')
  39. elif self.name.startswith('sr') or self.smart_support == 'Disabled':
  40. info_data = output.split('=== START OF INFORMATION SECTION ===')[1]
  41. smart_data = None
  42. else:
  43. info_data, smart_data = output.split('=== START OF READ SMART DATA SECTION ===')
  44. rex_health = search('(SMART overall-health self-assessment test result:|SMART Health Status:)\s*([\w\s\-!]+?)\n', output)
  45. if rex_health:
  46. self.smart_health = rex_health.group(2).strip()
  47. for m in findall('^(.+):\s+(.*)', info_data, flags=M):
  48. self.smart_info[m[0]] = m[1]
  49. if self.name.startswith('sd') and self.smart_support == 'Enabled':
  50. for m in findall('^\s*(\d+)\s*([\w-]+)\s+(\w*)\s*(\w*)\s*(\w*)\s*([\w-]+)\s*([\w-]*)\s*(\w*)\s*([\w\-!]*)\s*(\d+)', smart_data, flags=M):
  51. self.smart_data[m[0]] = {
  52. 'attribute_name': m[1],
  53. 'flag': m[2],
  54. 'value': m[3],
  55. 'worst': m[4],
  56. 'thresh': m[5],
  57. 'type': m[6],
  58. 'updated': m[7],
  59. 'when_failed': m[8],
  60. 'raw_value': m[9]
  61. }
  62. elif self.name.startswith('nvme'):
  63. for m in findall('^(.+):\s+(.*)', smart_data, flags=M):
  64. self.smart_data[m[0]] = m[1]
  65. if 'Vendor' in self.smart_info:
  66. self.vendor_model += self.smart_info['Vendor'].lower()
  67. if 'Model Family' in self.smart_info:
  68. self.vendor_model += self.smart_info['Model Family'].lower()
  69. if 'Product' in self.smart_info:
  70. self.vendor_model += self.smart_info['Product'].lower()
  71. except ValueError:
  72. pass
  73. def analyse(self, mode):
  74. if mode == 'lifetime':
  75. return self._lifetime()
  76. elif mode == 'runtime':
  77. return self._runtime()
  78. elif mode == 'rotation':
  79. return self._rotation()
  80. elif mode == 'size':
  81. return self._size()
  82. elif mode == 'health':
  83. return self._health()
  84. elif mode == 'written':
  85. return self._written()
  86. else:
  87. raise AttributeError('Unknown mode: {}'.format(mode))
  88. def _lifetime(self):
  89. if self.smart_data is {} or self.smart_info is {}:
  90. raise Exception('Please fetch smart data first')
  91. lifetime = None
  92. if self.name.startswith('nvme'):
  93. if 'Percentage Used' in self.smart_data:
  94. used = int(self.smart_data['Percentage Used'].split('%')[0])
  95. lifetime = 100 - used
  96. elif self.name.startswith('sd'):
  97. if 'samsung' in self.vendor_model:
  98. if '177' in self.smart_data: # Wear_Leveling_Count
  99. lifetime = int(self.smart_data['177']['value'])
  100. elif '173' in self.smart_data: # Wear_Leveling_Count
  101. lifetime = int(self.smart_data['173']['value'])
  102. if '179' in self.smart_data: # Used_Reserve_Block_Count
  103. smart_179 = int(self.smart_data['179']['raw_value'])
  104. if lifetime:
  105. if lifetime > smart_179:
  106. lifetime = smart_179
  107. else:
  108. lifetime = smart_179
  109. elif 'crucial' in self.vendor_model:
  110. if '202' in self.smart_data: # Remaining_lifetime_Perc or Percent_Lifetime_Used
  111. lifetime = int(self.smart_data['202']['raw_value'])
  112. elif 'ocz' in self.vendor_model:
  113. if '209' in self.smart_data: # Remaining_Lifetime_Perc
  114. lifetime = int(self.smart_data['209']['raw_value'])
  115. return lifetime
  116. def _health(self):
  117. health = self.smart_health
  118. sector_sum = 0
  119. if self.smart_support == 'Disabled':
  120. health = 'DSBLD'
  121. elif self.name.startswith('sd'):
  122. if 'SMART support is' in self.smart_info and self.smart_info['SMART support is'] == 'Disabled':
  123. health = 'DSBLD'
  124. if health == 'PASSED':
  125. if '199' in self.smart_data: # UDMA_CRC_Error_Count
  126. if int(self.smart_data['199']['raw_value']) >= 500:
  127. health = 'UDMA'
  128. for i in [str(i) for i in [5, 187, 197, 198] if str(i) in self.smart_data]: # 5:Reallocated_Sector_Ct, 187:Uncorrectable_Error_Cnt, 197:Current_Pending_Sector, 198:Uncorrectable_Sector_Count
  129. sector_sum += int(self.smart_data[i]['raw_value'])
  130. #: TODO -> INCORRECT!
  131. if 'crucial' in self.vendor_model and '172' in self.smart_data: # Erase_Fail_Count
  132. sector_sum += int(self.smart_data['172']['raw_value'])
  133. elif self.name.startswith('nvme'):
  134. if 'Critical Warning' in self.smart_data and self.smart_data['Critical Warning'] != '0x00':
  135. health = 'WARN'
  136. elif 'Warning Comp. Temperature Time' in self.smart_data and self.smart_data['Warning Comp. Temperature Time'] != '0':
  137. health = 'TEMP E'
  138. elif 'Warning Comp. Temperature Time' in self.smart_data and self.smart_data['Warning Comp. Temperature Time'] != '0':
  139. health = 'TEMP W'
  140. elif 'Media and Data Integrity Errors' in self.smart_data:
  141. sector_sum += int(self.smart_data['Media and Data Integrity Errors'])
  142. if sector_sum > 0:
  143. health = str(sector_sum)
  144. return health
  145. def _size(self):
  146. size = None
  147. raw_size = None
  148. if 'Total NVM Capacity' in self.smart_info:
  149. raw_size = self.smart_info['Total NVM Capacity']
  150. elif 'User Capacity' in self.smart_info:
  151. raw_size = self.smart_info['User Capacity']
  152. if raw_size:
  153. rex = search('\s*([\d\.]*)', raw_size.replace('.', '').replace(',', ''))
  154. if rex:
  155. size = rex.group(1)
  156. return size
  157. def _rotation(self):
  158. if self.name.startswith('nvme'):
  159. return 'NVMe'
  160. elif 'Rotation Rate' in self.smart_info:
  161. if self.smart_info['Rotation Rate'] == 'Solid State Device':
  162. return 'SSD'
  163. elif ' rpm' in self.smart_info['Rotation Rate']:
  164. return self.smart_info['Rotation Rate'].split(' rpm', 1)[0]
  165. return False
  166. def _runtime(self):
  167. hours = False
  168. if self.name.startswith('sd') and '9' in self.smart_data:
  169. hours = int(self.smart_data['9']['raw_value'])
  170. elif self.name.startswith('nvme') and 'Power On Hours' in self.smart_data:
  171. hours = int(self.smart_data['Power On Hours'])
  172. if hours:
  173. if hours < 24:
  174. if hours == 1:
  175. hours = '{} hour'.format(hours)
  176. else:
  177. hours = '{} hours'.format(hours)
  178. elif hours < 365 * 24:
  179. hours = '{:.1f} days'.format(hours / 24)
  180. else:
  181. hours = '{:.1f} years'.format(hours / 24 / 365)
  182. return hours
  183. def _written(self):
  184. raw_written = None
  185. if self.name.startswith('sd'):
  186. if '241' in self.smart_data:
  187. return self.smart_data['241']['raw_value'] # Total_LBAs_Written
  188. elif self.name.startswith('nvme'):
  189. if 'Data Units Written' in self.smart_data:
  190. raw_written = self.smart_data['Data Units Written']
  191. rex = search('\s*([\d\.]*)', raw_written)
  192. if rex:
  193. return rex.group(1)
  194. if __name__ == '__main__':
  195. dev = Device(argv[1])
  196. dev.fetch_smart()
  197. pprint(dev.smart_info)
  198. #print(dev.analyse('lifetime'))
  199. pprint(dev.smart_data)
  200. #print(dev.health.encode())
  201. #print(dev.support.encode())