From 5766fedc46ead6325851a0cded5e72ac16a59ff1 Mon Sep 17 00:00:00 2001 From: Adam Dej Date: Fri, 24 Nov 2017 20:07:10 +0100 Subject: [PATCH] [modules] Add zpool module --- README.md | 3 +- bumblebee/modules/zpool.py | 172 ++++++++++++++++++++++++++++++++ screenshots/zpool.png | Bin 0 -> 4934 bytes themes/icons/ascii.json | 7 ++ themes/icons/awesome-fonts.json | 8 +- 5 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 bumblebee/modules/zpool.py create mode 100644 screenshots/zpool.png 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 0000000000000000000000000000000000000000..c88bc0f7f571cacb4ada342ac71d178677101d24 GIT binary patch literal 4934 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 Ef+abn;s5{u literal 0 HcmV?d00001 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": "" },