diff --git a/README.md b/README.md index 3822e52..99785e5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) -**Many, many thanks to all contributors! As of now, 24 of the modules are from various contributors (!), and only 16 from myself.** +**Many, many thanks to all contributors! As of now, 25 of the modules are from various contributors (!), and only 16 from myself.** bumblebee-status is a modular, theme-able status line generator for the [i3 window manager](https://i3wm.org/). @@ -163,6 +163,7 @@ Modules and commandline utilities are only required for modules, the core itself * dbus-send (for module 'bluetooth') * nvidia-smi (for module 'nvidiagpu') * sensors (for module 'sensors', as fallback) +* zpool (for module 'zpool') # Examples Here are some screenshots for all themes that currently exist: diff --git a/bumblebee/modules/zpool.py b/bumblebee/modules/zpool.py new file mode 100644 index 0000000..9c43a67 --- /dev/null +++ b/bumblebee/modules/zpool.py @@ -0,0 +1,172 @@ +"""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 bumblebee.engine +from bumblebee.util import execute, bytefmt, asbool + + +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): + # zpool list -H: List all zpools, use script mode (no headers and tabs as separators). + 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" + 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") == 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 "" diff --git a/screenshots/zpool.png b/screenshots/zpool.png new file mode 100644 index 0000000..c88bc0f Binary files /dev/null and b/screenshots/zpool.png differ diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index 347fcfd..115bd52 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -89,5 +89,12 @@ }, "uptime": { "prefix": "uptime" + }, + "zpool": { + "poolread": {"prefix": "pool read "}, + "poolwrite": {"prefix": "pool write "}, + "ONLINE": {"prefix": "pool"}, + "FAULTED": {"prefix": "pool (!)"}, + "DEGRADED": {"prefix": "pool (!)"} } } diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index f8bd8cc..d5f1c97 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -19,7 +19,13 @@ "items": {"prefix": "" }, "uptime": {"prefix": "" } }, - + "zpool": { + "poolread": {"prefix": "→ "}, + "poolwrite": {"prefix": "← "}, + "ONLINE": {"prefix": ""}, + "FAULTED": {"prefix": "!"}, + "DEGRADED": {"prefix": "!"} + }, "cmus": { "playing": { "prefix": "" }, "paused": { "prefix": "" },