2020-04-25 10:27:06 +02:00
|
|
|
"""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
|
2020-04-25 10:27:27 +02:00
|
|
|
is displayed. (Default: '')
|
2020-04-25 10:27:06 +02:00
|
|
|
* zpool.format: Format string, tags {name}, {used}, {left}, {size}, {percentfree}, {percentuse},
|
|
|
|
{status}, {shortstatus}, {fragpercent}, {deduppercent} are supported.
|
2020-04-25 10:27:27 +02:00
|
|
|
(Default: '{name} {used}/{size} ({percentfree}%)')
|
2020-04-25 10:27:06 +02:00
|
|
|
* 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}
|
2020-04-25 10:27:27 +02:00
|
|
|
(bandwidth) are supported. (Default: '{band}')
|
2020-04-25 10:27:06 +02:00
|
|
|
* 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
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
2020-04-25 10:31:32 +02:00
|
|
|
import core.module
|
|
|
|
import core.widget
|
|
|
|
|
|
|
|
import util.cli
|
|
|
|
import util.format
|
|
|
|
|
|
|
|
class Module(core.module.Module):
|
2020-04-26 16:39:24 +02:00
|
|
|
def __init__(self, config, theme):
|
|
|
|
super().__init__(config, theme, [])
|
2020-04-25 10:27:06 +02:00
|
|
|
|
|
|
|
self._includelist = set(filter(lambda x: len(x) > 0,
|
2020-04-25 10:31:32 +02:00
|
|
|
util.format.aslist(self.parameter('list', default=''))))
|
2020-04-25 10:27:27 +02:00
|
|
|
self._format = self.parameter('format', default='{name} {shortstatus} {used}/{size} ' +
|
|
|
|
'({percentfree}%)')
|
2020-04-25 10:31:32 +02:00
|
|
|
self._usesudo = util.format.asbool(self.parameter('sudo', default=False))
|
|
|
|
self._showio = util.format.asbool(self.parameter('showio', default=True))
|
2020-04-25 10:27:27 +02:00
|
|
|
self._ioformat = self.parameter('ioformat', default='{band}')
|
|
|
|
self._warnfree = int(self.parameter('warnfree', default=10))
|
2020-04-25 10:27:06 +02:00
|
|
|
|
2020-04-25 10:31:32 +02:00
|
|
|
def update(self):
|
|
|
|
widgets = self.widgets()
|
2020-04-25 10:27:27 +02:00
|
|
|
zfs_version_path = '/sys/module/zfs/version'
|
2020-04-25 10:27:06 +02:00
|
|
|
# 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
|
2020-04-25 10:27:27 +02:00
|
|
|
zfs_version = '0.0.0'
|
|
|
|
logging.error('ZFS version information not found at {}, check the module is loaded.'.format(zfs_version_path))
|
2020-04-25 10:27:06 +02:00
|
|
|
|
2020-04-25 10:31:32 +02:00
|
|
|
raw_zpools = util.cli.execute(('sudo ' if self._usesudo else '') + 'zpool list -H').split('\n')
|
2020-04-25 10:27:06 +02:00
|
|
|
|
|
|
|
for widget in widgets:
|
2020-04-25 10:27:27 +02:00
|
|
|
widget.set('visited', False)
|
2020-04-25 10:27:06 +02:00
|
|
|
|
|
|
|
for raw_zpool in raw_zpools:
|
|
|
|
try:
|
2020-04-25 10:27:27 +02:00
|
|
|
# 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'):
|
2020-04-25 10:27:06 +02:00
|
|
|
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.
|
2020-04-25 10:27:27 +02:00
|
|
|
with open('/proc/spl/kstat/zfs/{}/io'.format(name), 'r') as f:
|
2020-04-25 10:27:06 +02:00
|
|
|
# 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:
|
2020-04-25 10:31:32 +02:00
|
|
|
widget = core.widget.Widget(name=name)
|
2020-04-25 10:27:27 +02:00
|
|
|
widget.set('last_iostat', [0, 0, 0, 0])
|
|
|
|
widget.set('last_timestamp', 0)
|
2020-04-25 10:27:06 +02:00
|
|
|
widgets.append(widget)
|
|
|
|
|
2020-04-25 10:27:27 +02:00
|
|
|
delta_iostat = [b - a for a, b in zip(iostat, widget.get('last_iostat'))]
|
|
|
|
widget.set('last_iostat', iostat)
|
2020-04-25 10:27:06 +02:00
|
|
|
|
|
|
|
# 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()
|
2020-04-25 10:27:27 +02:00
|
|
|
delta_timestamp = widget.get('last_timestamp') - timestamp
|
|
|
|
widget.set('last_timestamp', time.time())
|
2020-04-25 10:27:06 +02:00
|
|
|
|
|
|
|
# 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))
|
2020-04-25 10:27:27 +02:00
|
|
|
widget.set('state', health)
|
|
|
|
widget.set('percentfree', percentfree)
|
|
|
|
widget.set('visited', True)
|
2020-04-25 10:27:06 +02:00
|
|
|
|
|
|
|
if self._showio:
|
2020-04-25 10:27:27 +02:00
|
|
|
wname, rname = [name + x for x in ['__write', '__read']]
|
2020-04-25 10:27:06 +02:00
|
|
|
widget_w = self.widget(wname)
|
|
|
|
widget_r = self.widget(rname)
|
|
|
|
if not widget_w or not widget_r:
|
2020-04-25 10:31:32 +02:00
|
|
|
widget_r = core.widget.Widget(name=rname)
|
|
|
|
widget_w = core.widget.Widget(name=wname)
|
2020-04-25 10:27:06 +02:00
|
|
|
widgets.extend([widget_r, widget_w])
|
|
|
|
for w in [widget_r, widget_w]:
|
2020-04-25 10:27:27 +02:00
|
|
|
w.set('theme.minwidth', self._ioformat.format(ops=9999,
|
2020-04-25 10:31:32 +02:00
|
|
|
band=util.format.bytefmt(999.99*(1024**2))))
|
2020-04-25 10:27:27 +02:00
|
|
|
w.set('visited', True)
|
2020-04-25 10:27:06 +02:00
|
|
|
widget_w.full_text(self._ioformat.format(ops=round(writes),
|
2020-04-25 10:31:32 +02:00
|
|
|
band=util.format.bytefmt(nwritten)))
|
2020-04-25 10:27:06 +02:00
|
|
|
widget_r.full_text(self._ioformat.format(ops=round(reads),
|
2020-04-25 10:31:32 +02:00
|
|
|
band=util.format.bytefmt(nread)))
|
2020-04-25 10:27:06 +02:00
|
|
|
|
|
|
|
for widget in widgets:
|
2020-04-25 10:27:27 +02:00
|
|
|
if widget.get('visited') is False:
|
2020-04-25 10:27:06 +02:00
|
|
|
widgets.remove(widget)
|
2020-04-25 10:31:32 +02:00
|
|
|
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
|
2020-04-25 10:27:06 +02:00
|
|
|
|
|
|
|
@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 = {
|
2020-04-25 10:27:27 +02:00
|
|
|
'DEGRADED': 'DEG',
|
|
|
|
'FAULTED': 'FLT',
|
|
|
|
'ONLINE': 'ONL',
|
2020-04-25 10:27:06 +02:00
|
|
|
}
|
|
|
|
try:
|
|
|
|
return shortstate[status]
|
|
|
|
except KeyError:
|
2020-04-25 10:27:27 +02:00
|
|
|
return ''
|
2020-04-25 10:31:32 +02:00
|
|
|
|
|
|
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|