#!/usr/bin/python3 from argparse import ArgumentParser from os import listdir, getuid from shutil import copyfile, move from subprocess import TimeoutExpired, Popen from urllib.request import urlretrieve from devices import Device __version__ = '6.3.1' __versionDate__ = '2020-04-10' supported = ('sd', 'mmcblk', 'sr', 'vd', 'nvme') cols = [('Vendor / Model', 26), #25 ('Serial', 21), #16 ('Firmware', 10), #7 ('Size', 9), #7 ('Runtime', 12), #PoH 6 ('Written', 10), ('Rpm', 6), # 5 ('Life', 6), # 5 ('S.M.A.R.T.', 10)] alternatives = { 'model': ['/sys/block/{}/device/model', '/sys/block/{}/device/name'], 'serial': ['/sys/block/{}/device/serial'], 'firmware': ['/sys/block/{}/device/rev', '/sys/block/{}/device/fwrev'], 'size': ['/sys/block/{}/size'], 'vendor': ['/sys/block/{}/device/vendor']} hdparmRex = { 'model': b'\sModel=([\w\s\-]*)[\,\n]', 'firmware': b'\s*FwRev=([\w\s\-\.]*)[\,\n]', 'serial': b'\s*SerialNo=([\w\s\-]*)[\,\n]'} updateSmartUrl = 'https://raw.githubusercontent.com/mirror/smartmontools/master/drivedb.h' parser = ArgumentParser(description='List all conntected drives and monitore the S.M.A.R.T.-status', epilog='Baba {} ({}) by Schluggi'.format(__version__, __versionDate__)) parser.add_argument('device', help='only show specific device', nargs='?') parser.add_argument('-m', '--mib', help='show sizes in KiB, MiB, GiB, TiB and PiB', action='store_true') 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') parser.add_argument('-s', '--self-update', help='installs the newest version of baba', action='store_true') parser.add_argument('-t', '--timeout', help='the time to wait for a timeout in seconds (default 4)', nargs='?', default=4) parser.add_argument('-v', '--verbose', help='increase output verbosity', action='store_true') 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') args = parser.parse_args() def from_file(devname, keys): rv = '' if type(keys) is str: keys = [keys] for key in keys: for filename in alternatives[key]: try: with open(filename.format(devname)) as f: if rv: rv += ' ' rv += f.read().rstrip() except FileNotFoundError: pass if rv: return rv return '-' def update_drivedb(): """Downloading and update the drivedb.h""" print('Downloading new drivedb...', flush=True, end='') urlretrieve(updateSmartUrl, '/var/lib/smartmontools/drivedb/drivedb.h.new') print('OK\nBackuping current drivedb...', flush=True, end='') copyfile('/var/lib/smartmontools/drivedb/drivedb.h', '/var/lib/smartmontools/drivedb/drivedb.h.old') print('OK\nActivate new drivedb...', flush=True, end='') move('/var/lib/smartmontools/drivedb/drivedb.h.new', '/var/lib/smartmontools/drivedb/drivedb.h') print('OK\nFinish!') def calc_size(bytes, factor=1000, precision=0): units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] template = '{:.?f} {}'.replace('?', str(precision)) if factor == 1024: units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'] if bytes is 0: rv = '-' elif bytes < factor: rv = '{} {}'.format(bytes, units[0]) elif bytes < factor**2: rv = template.format(bytes/factor, units[1]) elif bytes < factor**3: rv = template.format(bytes/factor**2, units[2]) elif bytes < factor**4: rv = template.format(bytes/factor**3, units[3]) elif bytes < factor**5: rv = template.format(bytes/factor**4, units[4]) elif bytes < factor**6: rv = template.format(bytes/factor**5, units[5]) return rv def grabber(d, attributes): rv = '' for attr in attributes: if attr in d: if rv: rv += ' ' rv += d[attr] if rv: return rv return '-' def valuechecker(dev, factor): rv = {'vendor': '-', 'model': '-', 'serial': '-', 'firmware': '-', 'size': '-', 'runtime': '-', 'written': '-', 'rotation': '-', 'lifetime': '-', 'smart': '-'} device = Device(dev, args.timeout) try: device.fetch_smart() except TimeoutExpired: rv['smart'] = 'TIMEOUT' rv['lifetime'] = '?' rv['rotation'] = '?' rv['runtime'] = '?' rv['written'] = '?' if device.name.startswith(('nvme', 'sd', 'sr', 'vd')) and rv['smart'] != 'TIMEOUT': if device.name.startswith('nvme'): rv['model'] = grabber(device.smart_info, ['Model Number', 'Device Model']) elif device.name.startswith(('sd', 'vd')): rv['model'] = grabber(device.smart_info, ['Model Family', 'Vendor', 'Device Model', 'Product']) elif device.name.startswith('sr'): rv['model'] = grabber(device.smart_info, ['Vendor', 'Product']) rv['firmware'] = grabber(device.smart_info, ['Firmware Version', 'Revision']) rv['serial'] = grabber(device.smart_info, ['Serial Number', 'Serial number']) if device.name.startswith('sr') is False: rv['smart'] = device.analyse('health') size = device.analyse('size') if size: rv['size'] = calc_size(int(size), factor) runtime = device.analyse('runtime') if runtime: rv['runtime'] = runtime rotation = device.analyse('rotation') if rotation: rv['rotation'] = rotation lifetime = device.analyse('lifetime') if lifetime: rv['lifetime'] = lifetime written = device.analyse('written') if written: written = int(written.split(' [')[0].replace('.', '')) if device.name.startswith('nvme'): rv['written'] = calc_size(written*512*1000, factor, precision=1) else: if args.written: rv['written'] = calc_size(written*32000, factor, precision=1) else: rv['written'] = calc_size(written*512, factor, precision=1) if rv['model'] == '-': rv['model'] = from_file(device.name, ['vendor', 'model']) if rv['serial'] == '-': rv['serial'] = from_file(device.name, 'serial') if rv['firmware'] == '-': rv['firmware'] = from_file(device.name, 'firmware') if rv['size'] == '-' and device.name.startswith('sr') is False: size = int(from_file(device.name, 'size'))*512 rv['size'] = calc_size(size, factor=factor) return [rv['model'], rv['serial'], rv['firmware'], rv['size'], rv['runtime'], rv['written'], rv['rotation'], rv['lifetime'], rv['smart']] def short(s, max_len): if args.verbose: return '{} | '.format(s) elif len(s) > max_len: split_str = '[..]' split_len = int(max_len/2 - len(split_str)/2) return '{}{}{}'.format(s[:split_len], split_str, s[-split_len:]) else: return s def colored(color, s): if color == 'red': return '\x1b[0m\x1b[41m\x1b[1m{}\x1b[0m'.format(s) elif color == 'green': return '\x1b[0m\x1b[42m\x1b[1m{}\x1b[0m'.format(s) elif color == 'purple': return '\x1b[0m\x1b[45m\x1b[1m{}\x1b[0m'.format(s) elif color == 'blue': return '\x1b[0m\x1b[44m\x1b[1m{}\x1b[0m'.format(s) elif color == 'dark': return '\x1b[0m\x1b[40m\x1b[1m{}\x1b[0m'.format(s) elif color == 'turkey': return '\x1b[0m\x1b[46m\x1b[1m{}\x1b[0m'.format(s) elif color == 'yellow': return '\x1b[0m\x1b[1m\x1b[43m\x1b[30m{}\x1b[0m'.format(s) if getuid() != 0: exit('Please run as root!') elif args.update_drivedb: update_drivedb() exit() elif args.self_update: proc = Popen('/usr/share/baba/update.sh') if proc.wait() != 0: print('Oops. Please run /usr/share/baba/update.sh manually!') exit('Update finish!') elif args.device: if args.device.startswith('/dev/'): devices = [args.device.split('/')[-1]] else: devices = [args.device] else: devices = [f for f in sorted(listdir('/sys/block/'), key=lambda x: (len(x), x)) if f.startswith(supported)] factor = 1000 if args.mib: factor = 1024 print('\x1b[1m{}'.format('Device'.ljust(8)), end='', flush=False) for c in cols: print(c[0].ljust(c[1]), end='', flush=False) print('\x1b[0m') for lno, filename in enumerate(devices): #: colored lines if lno % 2: print('\x1b[33m', end='') else: print('\x1b[36m', end='') #: print device name print(filename.ljust(8), flush=True, end='') #: get and print the other values for i, value in enumerate(valuechecker('/dev/{}'.format(filename), factor=factor)): if value is None: value = '-' if i is 8: # smart if value in ('PASSED', 'OK'): value = colored('green', ' OK ') elif value == 'DSBLD': value = colored('red', ' DISABLED ') elif value == 'UDMA': value = colored('red', ' UltraDMA') elif value == 'TIMEOUT': value = colored('purple', ' TIME-OUT ') elif value == 'UNKNOWN': value = colored('blue', ' UNKNOWN ') elif value == 'USBB': value = colored('blue', 'USB-BRIDGE') elif value == '-': value = colored('dark', ' NO SMART ') elif value != '-': value = colored('red', value.ljust(cols[i][1])) print(value, end='') elif i is 7 and value not in ['-', '?']: # lifetime value_str = str(value) just = cols[i][1] - len(value_str) - 1 if value <= 45: value = colored('red', '{}%'.format(value)) elif value < 80: value = colored('yellow', '{}%'.format(value)) else: value = colored('green', '{}%'.format(value)) print(value.ljust(len(value) + just), end='') else: print(short(value, cols[i][1]-1).ljust(cols[i][1]), end='') print('\x1b[0m') exit()