S.M.A.R.T. drive checking has never been easier
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

347 рядки
11 KiB

  1. #!/usr/bin/python3
  2. from argparse import ArgumentParser
  3. from os import listdir, getuid
  4. from shutil import copyfile, move
  5. from subprocess import TimeoutExpired, Popen
  6. from urllib.request import urlretrieve
  7. from devices import Device
  8. __version__ = '6.3.1'
  9. __versionDate__ = '2020-04-10'
  10. supported = ('sd', 'mmcblk', 'sr', 'vd', 'nvme')
  11. cols = [('Vendor / Model', 26), #25
  12. ('Serial', 21), #16
  13. ('Firmware', 10), #7
  14. ('Size', 9), #7
  15. ('Runtime', 12), #PoH 6
  16. ('Written', 10),
  17. ('Rpm', 6), # 5
  18. ('Life', 6), # 5
  19. ('S.M.A.R.T.', 10)]
  20. alternatives = { 'model': ['/sys/block/{}/device/model', '/sys/block/{}/device/name'],
  21. 'serial': ['/sys/block/{}/device/serial'],
  22. 'firmware': ['/sys/block/{}/device/rev', '/sys/block/{}/device/fwrev'],
  23. 'size': ['/sys/block/{}/size'],
  24. 'vendor': ['/sys/block/{}/device/vendor']}
  25. hdparmRex = { 'model': b'\sModel=([\w\s\-]*)[\,\n]',
  26. 'firmware': b'\s*FwRev=([\w\s\-\.]*)[\,\n]',
  27. 'serial': b'\s*SerialNo=([\w\s\-]*)[\,\n]'}
  28. updateSmartUrl = 'https://raw.githubusercontent.com/mirror/smartmontools/master/drivedb.h'
  29. parser = ArgumentParser(description='List all conntected drives and monitore the S.M.A.R.T.-status', epilog='Baba {} ({}) by Schluggi'.format(__version__, __versionDate__))
  30. parser.add_argument('device', help='only show specific device', nargs='?')
  31. parser.add_argument('-m', '--mib', help='show sizes in KiB, MiB, GiB, TiB and PiB', action='store_true')
  32. parser.add_argument('-u', '--update-drivedb', help='updating drivedb.h to increase the S.M.A.R.T. compatibility. This is equal to "update-smart-drivedb"', action='store_true')
  33. parser.add_argument('-s', '--self-update', help='installs the newest version of baba', action='store_true')
  34. parser.add_argument('-t', '--timeout', help='the time to wait for a timeout in seconds (default 4)', nargs='?', default=4)
  35. parser.add_argument('-v', '--verbose', help='increase output verbosity', action='store_true')
  36. parser.add_argument('-w', '--written', help='use 32 KB LBAs instead of the default 512 Bytes to calculate the "written" value (only for non-nvme devices)', action='store_true')
  37. args = parser.parse_args()
  38. def from_file(devname, keys):
  39. rv = ''
  40. if type(keys) is str:
  41. keys = [keys]
  42. for key in keys:
  43. for filename in alternatives[key]:
  44. try:
  45. with open(filename.format(devname)) as f:
  46. if rv:
  47. rv += ' '
  48. rv += f.read().rstrip()
  49. except FileNotFoundError:
  50. pass
  51. if rv:
  52. return rv
  53. return '-'
  54. def update_drivedb():
  55. """Downloading and update the drivedb.h"""
  56. print('Downloading new drivedb...', flush=True, end='')
  57. urlretrieve(updateSmartUrl, '/var/lib/smartmontools/drivedb/drivedb.h.new')
  58. print('OK\nBackuping current drivedb...', flush=True, end='')
  59. copyfile('/var/lib/smartmontools/drivedb/drivedb.h', '/var/lib/smartmontools/drivedb/drivedb.h.old')
  60. print('OK\nActivate new drivedb...', flush=True, end='')
  61. move('/var/lib/smartmontools/drivedb/drivedb.h.new', '/var/lib/smartmontools/drivedb/drivedb.h')
  62. print('OK\nFinish!')
  63. def calc_size(bytes, factor=1000, precision=0):
  64. units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
  65. template = '{:.?f} {}'.replace('?', str(precision))
  66. if factor == 1024:
  67. units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']
  68. if bytes is 0:
  69. rv = '-'
  70. elif bytes < factor:
  71. rv = '{} {}'.format(bytes, units[0])
  72. elif bytes < factor**2:
  73. rv = template.format(bytes/factor, units[1])
  74. elif bytes < factor**3:
  75. rv = template.format(bytes/factor**2, units[2])
  76. elif bytes < factor**4:
  77. rv = template.format(bytes/factor**3, units[3])
  78. elif bytes < factor**5:
  79. rv = template.format(bytes/factor**4, units[4])
  80. elif bytes < factor**6:
  81. rv = template.format(bytes/factor**5, units[5])
  82. return rv
  83. def grabber(d, attributes):
  84. rv = ''
  85. for attr in attributes:
  86. if attr in d:
  87. if rv:
  88. rv += ' '
  89. rv += d[attr]
  90. if rv:
  91. return rv
  92. return '-'
  93. def valuechecker(dev, factor):
  94. rv = {'vendor': '-',
  95. 'model': '-',
  96. 'serial': '-',
  97. 'firmware': '-',
  98. 'size': '-',
  99. 'runtime': '-',
  100. 'written': '-',
  101. 'rotation': '-',
  102. 'lifetime': '-',
  103. 'smart': '-'}
  104. device = Device(dev, args.timeout)
  105. try:
  106. device.fetch_smart()
  107. except TimeoutExpired:
  108. rv['smart'] = 'TIMEOUT'
  109. rv['lifetime'] = '?'
  110. rv['rotation'] = '?'
  111. rv['runtime'] = '?'
  112. rv['written'] = '?'
  113. if device.name.startswith(('nvme', 'sd', 'sr', 'vd')) and rv['smart'] != 'TIMEOUT':
  114. if device.name.startswith('nvme'):
  115. rv['model'] = grabber(device.smart_info, ['Model Number', 'Device Model'])
  116. elif device.name.startswith(('sd', 'vd')):
  117. rv['model'] = grabber(device.smart_info, ['Model Family', 'Vendor', 'Device Model', 'Product'])
  118. elif device.name.startswith('sr'):
  119. rv['model'] = grabber(device.smart_info, ['Vendor', 'Product'])
  120. rv['firmware'] = grabber(device.smart_info, ['Firmware Version', 'Revision'])
  121. rv['serial'] = grabber(device.smart_info, ['Serial Number', 'Serial number'])
  122. if device.name.startswith('sr') is False:
  123. rv['smart'] = device.analyse('health')
  124. size = device.analyse('size')
  125. if size:
  126. rv['size'] = calc_size(int(size), factor)
  127. runtime = device.analyse('runtime')
  128. if runtime:
  129. rv['runtime'] = runtime
  130. rotation = device.analyse('rotation')
  131. if rotation:
  132. rv['rotation'] = rotation
  133. lifetime = device.analyse('lifetime')
  134. if lifetime:
  135. rv['lifetime'] = lifetime
  136. written = device.analyse('written')
  137. if written:
  138. written = int(written.split(' [')[0].replace('.', ''))
  139. if device.name.startswith('nvme'):
  140. rv['written'] = calc_size(written*512*1000, factor, precision=1)
  141. else:
  142. if args.written:
  143. rv['written'] = calc_size(written*32000, factor, precision=1)
  144. else:
  145. rv['written'] = calc_size(written*512, factor, precision=1)
  146. if rv['model'] == '-':
  147. rv['model'] = from_file(device.name, ['vendor', 'model'])
  148. if rv['serial'] == '-':
  149. rv['serial'] = from_file(device.name, 'serial')
  150. if rv['firmware'] == '-':
  151. rv['firmware'] = from_file(device.name, 'firmware')
  152. if rv['size'] == '-' and device.name.startswith('sr') is False:
  153. size = int(from_file(device.name, 'size'))*512
  154. rv['size'] = calc_size(size, factor=factor)
  155. return [rv['model'],
  156. rv['serial'],
  157. rv['firmware'],
  158. rv['size'],
  159. rv['runtime'],
  160. rv['written'],
  161. rv['rotation'],
  162. rv['lifetime'],
  163. rv['smart']]
  164. def short(s, max_len):
  165. if args.verbose:
  166. return '{} | '.format(s)
  167. elif len(s) > max_len:
  168. split_str = '[..]'
  169. split_len = int(max_len/2 - len(split_str)/2)
  170. return '{}{}{}'.format(s[:split_len], split_str, s[-split_len:])
  171. else:
  172. return s
  173. def colored(color, s):
  174. if color == 'red':
  175. return '\x1b[0m\x1b[41m\x1b[1m{}\x1b[0m'.format(s)
  176. elif color == 'green':
  177. return '\x1b[0m\x1b[42m\x1b[1m{}\x1b[0m'.format(s)
  178. elif color == 'purple':
  179. return '\x1b[0m\x1b[45m\x1b[1m{}\x1b[0m'.format(s)
  180. elif color == 'blue':
  181. return '\x1b[0m\x1b[44m\x1b[1m{}\x1b[0m'.format(s)
  182. elif color == 'dark':
  183. return '\x1b[0m\x1b[40m\x1b[1m{}\x1b[0m'.format(s)
  184. elif color == 'turkey':
  185. return '\x1b[0m\x1b[46m\x1b[1m{}\x1b[0m'.format(s)
  186. elif color == 'yellow':
  187. return '\x1b[0m\x1b[1m\x1b[43m\x1b[30m{}\x1b[0m'.format(s)
  188. if getuid() != 0:
  189. exit('Please run as root!')
  190. elif args.update_drivedb:
  191. update_drivedb()
  192. exit()
  193. elif args.self_update:
  194. proc = Popen('/usr/share/baba/update.sh')
  195. if proc.wait() != 0:
  196. print('Oops. Please run /usr/share/baba/update.sh manually!')
  197. exit('Update finish!')
  198. elif args.device:
  199. if args.device.startswith('/dev/'):
  200. devices = [args.device.split('/')[-1]]
  201. else:
  202. devices = [args.device]
  203. else:
  204. devices = [f for f in sorted(listdir('/sys/block/'), key=lambda x: (len(x), x)) if f.startswith(supported)]
  205. factor = 1000
  206. if args.mib:
  207. factor = 1024
  208. print('\x1b[1m{}'.format('Device'.ljust(8)), end='', flush=False)
  209. for c in cols:
  210. print(c[0].ljust(c[1]), end='', flush=False)
  211. print('\x1b[0m')
  212. for lno, filename in enumerate(devices):
  213. #: colored lines
  214. if lno % 2:
  215. print('\x1b[33m', end='')
  216. else:
  217. print('\x1b[36m', end='')
  218. #: print device name
  219. print(filename.ljust(8), flush=True, end='')
  220. #: get and print the other values
  221. for i, value in enumerate(valuechecker('/dev/{}'.format(filename), factor=factor)):
  222. if value is None:
  223. value = '-'
  224. if i is 8: # smart
  225. if value in ('PASSED', 'OK'):
  226. value = colored('green', ' OK ')
  227. elif value == 'DSBLD':
  228. value = colored('red', ' DISABLED ')
  229. elif value == 'UDMA':
  230. value = colored('red', ' UltraDMA')
  231. elif value == 'TIMEOUT':
  232. value = colored('purple', ' TIME-OUT ')
  233. elif value == 'UNKNOWN':
  234. value = colored('blue', ' UNKNOWN ')
  235. elif value == 'USBB':
  236. value = colored('blue', 'USB-BRIDGE')
  237. elif value == '-':
  238. value = colored('dark', ' NO SMART ')
  239. elif value != '-':
  240. value = colored('red', value.ljust(cols[i][1]))
  241. print(value, end='')
  242. elif i is 7 and value not in ['-', '?']: # lifetime
  243. value_str = str(value)
  244. just = cols[i][1] - len(value_str) - 1
  245. if value <= 45:
  246. value = colored('red', '{}%'.format(value))
  247. elif value < 80:
  248. value = colored('yellow', '{}%'.format(value))
  249. else:
  250. value = colored('green', '{}%'.format(value))
  251. print(value.ljust(len(value) + just), end='')
  252. else:
  253. print(short(value, cols[i][1]-1).ljust(cols[i][1]), end='')
  254. print('\x1b[0m')
  255. exit()