Linux
banshee/linux-collection/linux/ssh/
filesystems
Space, inodes and read-only state of each mounted filesystem of a Linux by SSH.
by banshee1 installation3 versions
v3Latest
Published on October 28, 2026. One naming convention across collections; documentation completed.
// Space and inodes of each mounted filesystem of a Linux, by SSH, and those that turn read-only (an ext4
// that meets an error remounts itself so); memory and pseudo filesystems are left out, and an expected
// mount that disappears shows through "Filesystem missing".
// Reads /proc/mounts with cat, for the type and mode of each mount (the kernel writes it the same way on
// Alpine, Debian and Fedora), then space and inodes with df -P -k and df -P -i: -P (the POSIX format, one
// line per filesystem), -k and -i are options of BusyBox as of GNU coreutils. A df that does not know -i
// (a BusyBox built without it) stops the collector: inodes would not go silent without a sound.
// Usage is the one df shows: used space over reachable space (used and available), root's reserve excluded.
need('protocol:ssh');
per('filesystem', 'Filesystems');
output('used-percent', 'number', array('label' => 'Used space', 'unit' => '%', 'per' => 'filesystem'));
output('used', 'number', array('label' => 'Used space', 'unit' => 'GiB', 'per' => 'filesystem'));
output('free', 'number', array('label' => 'Free space', 'unit' => 'GiB', 'per' => 'filesystem'));
output('size', 'number', array('label' => 'Size', 'unit' => 'GiB', 'per' => 'filesystem'));
output('inodes-used', 'number', array('label' => 'Used inodes', 'unit' => '%', 'per' => 'filesystem'));
output('inodes-free', 'number', array('label' => 'Free inodes', 'per' => 'filesystem'));
output('mode', 'text', array('label' => 'Mount mode', 'per' => 'filesystem'));
output('access', 'text', array('label' => 'Access', 'per' => 'filesystem'));
watch.above('used', 'Used space', 'used-percent', array('warning' => 80, 'critical' => 90));
watch.above('full-soon', 'Full within a week', 'used-percent', array('warning' => 99, 'ahead' => '7 d'));
watch.below('free', 'Little free space', 'free', array('critical' => 5, 'enabled' => false));
watch.above('inodes', 'Inodes running out', 'inodes-used', array('warning' => 80, 'critical' => 90));
watch.state('read-only', 'Mounted read-only', 'mode', array('critical' => array('ro')));
watch.state('unreadable', 'Filesystem unreadable', 'access', array('warning' => array('unreadable')));
watch.missing('missing', 'Filesystem missing', 'used-percent', array('enabled' => false));
// Types that are not disks: in memory, layered (containers), images (snap, CD), firmware's own, or the
// views a desktop session mounts for its user alone (gvfs, the document portal).
// Pseudo filesystems of size zero (proc, sysfs, cgroup...) are left out below by their size.
$pseudo = array('tmpfs' => true, 'devtmpfs' => true, 'ramfs' => true, 'overlay' => true, 'squashfs' => true,
'iso9660' => true, 'udf' => true, 'efivarfs' => true, 'fuse.lxcfs' => true, 'fuse.gvfsd-fuse' => true,
'fuse.portal' => true);
// /proc/mounts, one line per mount: "/dev/sda1 / ext4 rw,relatime 0 0"; a blank in a mount point is
// written \040 there.
$mounts = ssh.run('cat /proc/mounts');
$type_of = array();
$mode_of = array();
foreach (text.lines($mounts['output']) as $line) {
$w = text.words($line);
if (list.count($w) < 4) {
continue;
}
$point = text.join(text.split($w[1], '\040'), ' ');
$options = text.split($w[3], ',');
$type_of[$point] = $w[2];
$mode_of[$point] = $options[0];
}
if (list.count($type_of) == 0) {
fail('misconfigured', 'No mount found in /proc/mounts', $mounts['errors']);
}
// df -P -k, after the header: "Filesystem 1024-blocks Used Available Capacity Mounted-on", sizes in KiB;
// the mount point is the rest of the line (a run of blanks in its name becomes a single one). A df that
// cannot read a mount leaves it out of these lines and exits with an error: the other lines stay good.
$space = ssh.run('df -P -k');
$lines = text.lines($space['output']);
$kept = array();
for ($i = 1; $i < list.count($lines); $i = $i + 1) {
$w = text.words($lines[$i]);
if (list.count($w) < 6) {
continue;
}
$point = $w[5];
for ($j = 6; $j < list.count($w); $j = $j + 1) {
$point = $point . ' ' . $w[$j];
}
// A mount point may show twice (two stacked mounts): only the first is judged.
if (list.has($kept, $point) || list.has($pseudo, list.get($type_of, $point, '')) || $w[1] <= 0) {
continue;
}
$kept[$point] = true;
if ($w[2] + $w[3] > 0) {
value('used-percent', number.round(100 * $w[2] / ($w[2] + $w[3]), 1), $point);
}
value('used', number.round($w[2] / 1048576, 1), $point);
value('free', number.round($w[3] / 1048576, 1), $point);
value('size', number.round($w[1] / 1048576, 1), $point);
value('mode', list.get($mode_of, $point, 'rw'), $point);
value('access', 'readable', $point);
}
// A mount df could not read, because the account has no right to its mount point (the network or FUSE
// mount of another user), is named on df's error output: "df: /mnt/x: Permission denied", the point between
// quotes or not as the version of df writes it. It is returned unreadable, never left out in silence.
$unreadable = array();
foreach (text.lines($space['errors']) as $line) {
$m = text.match($line, "^df: [‘']?(.*?)[’']?: [^:]+$");
if (list.count($m) == 0) {
continue;
}
$point = $m[1];
if (!list.has($type_of, $point) || list.has($pseudo, $type_of[$point]) || list.has($kept, $point)
|| list.has($unreadable, $point)) {
continue;
}
$unreadable[$point] = true;
value('mode', $mode_of[$point], $point);
value('access', 'unreadable', $point);
}
if (list.count($kept) + list.count($unreadable) == 0) {
fail('misconfigured', 'No filesystem found in the output of df -P -k', $space['output'] . $space['errors']);
}
// df -P -i, same layout: "Filesystem Inodes IUsed IFree IUse% Mounted-on". A filesystem that does not
// count inodes (btrfs, vfat) reports zero, and has nothing to judge.
$inodes = ssh.run('df -P -i');
$lines = text.lines($inodes['output']);
if (list.count($lines) < 2) {
fail('misconfigured', 'df -P -i returned nothing: this df does not count inodes', $inodes['errors']);
}
$judged = array();
for ($i = 1; $i < list.count($lines); $i = $i + 1) {
$w = text.words($lines[$i]);
if (list.count($w) < 6) {
continue;
}
$point = $w[5];
for ($j = 6; $j < list.count($w); $j = $j + 1) {
$point = $point . ' ' . $w[$j];
}
if (!list.has($kept, $point) || list.has($judged, $point) || $w[1] <= 0) {
continue;
}
$judged[$point] = true;
value('inodes-used', number.round(100 * $w[2] / $w[1], 1), $point);
value('inodes-free', $w[3], $point);
}