"""Displays info about zpools present on the system Parameters: * zpool.list: Comma-separated list of zpools to display info for. If empty, info for all zpools is displayed. (Default: '') * zpool.format: Format string, tags {name}, {used}, {left}, {size}, {percentfree}, {percentuse}, {status}, {shortstatus}, {fragpercent}, {deduppercent} are supported. (Default: '{name} {used}/{size} ({percentfree}%)') * zpool.showio: Show also widgets detailing current read and write I/O (Default: true) * zpool.ioformat: Format string for I/O widget, tags {ops} (operations per seconds) and {band} (bandwidth) are supported. (Default: '{band}') * zpool.warnfree: Warn if free space is below this percentage (Default: 10) * zpool.sudo: Use sudo when calling the `zpool` binary. (Default: false) Option `zpool.sudo` is intended for Linux users using zfsonlinux older than 0.7.0: In pre-0.7.0 releases of zfsonlinux regular users couldn't invoke even informative commands such as `zpool list`. If this option is true, command `zpool list` is invoked with sudo. If this option is used, the following (or ekvivalent) must be added to the `sudoers(5)`: ``` ALL = (root) NOPASSWD: /usr/bin/zpool list ``` Be aware of security implications of doing this! """ import time import logging from pkg_resources import parse_version log = logging.getLogger(__name__) import core.module import core.widget import util.cli import util.format class Module(core.module.Module): def __init__(self, config, theme): super().__init__(config, theme, []) self._includelist = set(filter(lambda x: len(x) > 0, util.format.aslist(self.parameter('list', default='')))) self._format = self.parameter('format', default='{name} {shortstatus} {used}/{size} ' + '({percentfree}%)') self._usesudo = util.format.asbool(self.parameter('sudo', default=False)) self._showio = util.format.asbool(self.parameter('showio', default=True)) self._ioformat = self.parameter('ioformat', default='{band}') self._warnfree = int(self.parameter('warnfree', default=10)) def update(self): widgets = self.widgets() zfs_version_path = '/sys/module/zfs/version' # zpool list -H: List all zpools, use script mode (no headers and tabs as separators). try: with open(zfs_version_path, 'r') as zfs_mod_version: zfs_version = zfs_mod_version.readline().rstrip().split('-')[0] except IOError: # ZFS isn't installed or the module isn't loaded, stub the version zfs_version = '0.0.0' logging.error('ZFS version information not found at {}, check the module is loaded.'.format(zfs_version_path)) raw_zpools = util.cli.execute(('sudo ' if self._usesudo else '') + 'zpool list -H').split('\n') for widget in widgets: widget.set('visited', False) for raw_zpool in raw_zpools: try: # Ignored fields (assigned to _) are 'expandsz' and 'altroot', also 'ckpoint' in ZFS 0.8.0+ if parse_version(zfs_version) < parse_version('0.8.0'): name, size, alloc, free, _, frag, cap, dedup, health, _ = raw_zpool.split('\t') else: name, size, alloc, free, _, _, frag, cap, dedup, health, _ = raw_zpool.split('\t') cap = cap.rstrip('%') percentuse=int(cap) percentfree=100-percentuse # There is a command, zpool iostat, which is however blocking and was therefore # causing issues. # Instead, we read file `/proc/spl/kstat/zfs//io` which contains # cumulative I/O statistics since boot (or pool creation). We store these values # (and timestamp) during each widget update, and during the next widget update we # use them to compute delta of transferred bytes, and using the last and current # timestamp the rate at which they have been transferred. with open('/proc/spl/kstat/zfs/{}/io'.format(name), 'r') as f: # Third row provides data we need, we are interested in the first 4 values. # More info about this file can be found here: # https://github.com/zfsonlinux/zfs/blob/master/lib/libspl/include/sys/kstat.h#L580 # The 4 values are: # nread, nwritten, reads, writes iostat = list(map(int, f.readlines()[2].split()[:4])) except (ValueError, IOError): # Unable to parse info about this pool, skip it continue if self._includelist and name not in self._includelist: continue widget = self.widget(name) if not widget: widget = core.widget.Widget(name=name) widget.set('last_iostat', [0, 0, 0, 0]) widget.set('last_timestamp', 0) widgets.append(widget) delta_iostat = [b - a for a, b in zip(iostat, widget.get('last_iostat'))] widget.set('last_iostat', iostat) # From docs: # > Note that even though the time is always returned as a floating point number, not # > all systems provide time with a better precision than 1 second. # Be aware that that may affect the precision of reported I/O # Also, during one update cycle the reported I/O may be garbage if the system time # was changed. timestamp = time.time() delta_timestamp = widget.get('last_timestamp') - timestamp widget.set('last_timestamp', time.time()) # abs is there because sometimes the result is -0 rate_iostat = [abs(x / delta_timestamp) for x in delta_iostat] nread, nwritten, reads, writes = rate_iostat # theme.minwidth is not set since these values are not expected to change # rapidly widget.full_text(self._format.format(name=name, used=alloc, left=free, size=size, percentfree=percentfree, percentuse=percentuse, status=health, shortstatus=self._shortstatus(health), fragpercent=frag, deduppercent=dedup)) widget.set('state', health) widget.set('percentfree', percentfree) widget.set('visited', True) if self._showio: wname, rname = [name + x for x in ['__write', '__read']] widget_w = self.widget(wname) widget_r = self.widget(rname) if not widget_w or not widget_r: widget_r = core.widget.Widget(name=rname) widget_w = core.widget.Widget(name=wname) widgets.extend([widget_r, widget_w]) for w in [widget_r, widget_w]: w.set('theme.minwidth', self._ioformat.format(ops=9999, band=util.format.bytefmt(999.99*(1024**2)))) w.set('visited', True) widget_w.full_text(self._ioformat.format(ops=round(writes), band=util.format.bytefmt(nwritten))) widget_r.full_text(self._ioformat.format(ops=round(reads), band=util.format.bytefmt(nread))) for widget in widgets: if widget.get('visited') is False: widgets.remove(widget) self.widgets(widgets) def state(self, widget): if widget.name.endswith('__read'): return 'poolread' elif widget.name.endswith('__write'): return 'poolwrite' state = widget.get('state') if state == 'FAULTED': return [state, 'critical'] elif state == 'DEGRADED' or widget.get('percentfree') < self._warnfree: return [state, 'warning'] return state @staticmethod def _shortstatus(status): # From `zpool(8)`, section Device Failure and Recovery: # A pool's health status is described by one of three states: online, degraded, or faulted. # An online pool has all devices operating normally. A degraded pool is one in which one # or more devices have failed, but the data is still available due to a redundant # configuration. A faulted pool has corrupted metadata, or one or more faulted devices, and # insufficient replicas to continue functioning. shortstate = { 'DEGRADED': 'DEG', 'FAULTED': 'FLT', 'ONLINE': 'ONL', } try: return shortstate[status] except KeyError: return '' # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4