diff --git a/modules/contrib/zpool.py b/modules/contrib/zpool.py new file mode 100644 index 0000000..1f3e897 --- /dev/null +++ b/modules/contrib/zpool.py @@ -0,0 +1,187 @@ +"""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 +import bumblebee.engine +from bumblebee.util import execute, bytefmt, asbool + +log = logging.getLogger(__name__) + +class Module(bumblebee.engine.Module): + def __init__(self, engine, config): + widgets = [] + super(Module, self).__init__(engine, config, widgets) + + self._includelist = set(filter(lambda x: len(x) > 0, + self.parameter("list", default="").split(','))) + self._format = self.parameter("format", default="{name} {shortstatus} {used}/{size} " + + "({percentfree}%)") + self._usesudo = asbool(self.parameter("sudo", default=False)) + self._showio = asbool(self.parameter("showio", default=True)) + self._ioformat = self.parameter("ioformat", default="{band}") + self._warnfree = int(self.parameter("warnfree", default=10)) + + self._update_widgets(widgets) + + def update(self, widgets): + self._update_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 + + def _update_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 = 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 = bumblebee.output.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 = bumblebee.output.Widget(name=rname) + widget_w = bumblebee.output.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=bytefmt(999.99*(1024**2)))) + w.set("visited", True) + widget_w.full_text(self._ioformat.format(ops=round(writes), + band=bytefmt(nwritten))) + widget_r.full_text(self._ioformat.format(ops=round(reads), + band=bytefmt(nread))) + + for widget in widgets: + if widget.get("visited") is False: + widgets.remove(widget) + + @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 ""