bumblebee-status/bumblebee/modules/zpool.py
Alex Boag-Munroe e44eea3318 Log when unable to ascertain ZFS version
Use established logging strategy to emit an error log when ZFS
version information cannot be obtained.
2019-09-26 23:30:06 +01:00

187 lines
9 KiB
Python

"""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)`:
```
<username/ALL> 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 FileNotFoundError:
# 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/<poolname>/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 ""