"""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! contributed by `adam-dej `_ - many thanks! """ 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