bundlewrap/bundles/sshmon/files/check_mounts
2023-02-05 17:30:58 +01:00

153 lines
4.1 KiB
Python

#!/usr/bin/env python3
from argparse import ArgumentParser
from subprocess import check_output
from tempfile import TemporaryFile
check_filesystem_types = {
'ext2',
'ext3',
'ext4',
'vfat',
}
def read_systemd():
"""
Read configured mount units from systemd.
"""
lines = check_output(
'systemctl list-unit-files -at mount --no-legend --no-pager',
shell=True,
).decode('UTF-8').splitlines()
for line in lines:
frag_path = None
fstype = None
options = None
source_path = None
state = None
where = None
mountunit = line.split()[0]
props = check_output(
"systemctl show -p FragmentPath,Options,SourcePath,Type,UnitFileState,Where -- '{}'".format(mountunit),
shell=True,
).decode('UTF-8')
for pline in props.splitlines():
if pline.startswith('FragmentPath='):
frag_path = pline[len('FragmentPath='):]
elif pline.startswith('Options='):
options = pline[len('Options='):]
elif pline.startswith('SourcePath='):
source_path = pline[len('SourcePath='):]
elif pline.startswith('Type='):
fstype = pline[len('Type='):]
elif pline.startswith('UnitFileState='):
state = pline[len('UnitFileState='):]
elif pline.startswith('Where='):
where = pline[len('Where='):]
if state not in ('enabled', 'generated', 'static'):
continue
# The properties of mount units change once they are mounted.
# For example, "options" and "type" change from "bind"/"none" to
# something like "ext4"/"rw,relatime" once a bind-mount is
# mounted.
#
# fstype can be an empty string if an admin decides to simply
# not specify the type in its mount unit. (Only good old fstab
# forced setting fstype.)
if (
options != 'bind' and
fstype != '' and
fstype not in check_filesystem_types
):
continue
# Traditional mountpoints, those are represented by systemd
# units which are auto-generated.
if source_path == '/etc/fstab':
yield where
# Okay, this is a real systemd mount unit. Has it been
# configured by an admin or is it noise?
elif frag_path.startswith('/etc/systemd/system'):
yield where
def read_unix(path):
"""
Read /etc/fstab or /proc/self/mounts.
"""
with open(path, 'r') as fp:
lines = fp.read().splitlines()
for line in lines:
line = line.strip()
if line.startswith('#'):
continue
fields = line.split()
if len(fields) < 3 or fields[2] not in check_filesystem_types:
continue
# Only the mountpoint.
yield fields[1]
def rwtest(path):
try:
with TemporaryFile(dir=path) as fp:
pass
except Exception:
return False
return True
parser = ArgumentParser()
parser.add_argument('--ignore', nargs='*')
args = parser.parse_args()
# read_systemd() does not return everything on systems older than 18.04.
configured = set(read_systemd()) | set(read_unix('/etc/fstab'))
mounted = set(read_unix('/proc/self/mounts'))
configured -= set(args.ignore or [])
mounted -= set(args.ignore or [])
missing_mounted = configured - mounted
missing_configured = mounted - configured
mounted_as_configured = mounted & configured
all_mounts = configured | mounted
not_okay = {}
for i in missing_mounted:
not_okay[i] = 'not mounted'
for i in missing_configured:
not_okay[i] = 'not in fstab nor systemd unit'
for i in mounted_as_configured:
if not rwtest(i):
not_okay[i] = 'mounted read-only'
exitcode = 0
# Two loops to have CRITICAL printed before OK without having to create
# a new data structure.
for i in sorted(all_mounts):
if i in not_okay:
print('CRITICAL - {}: {}'.format(i, not_okay[i]))
exitcode = 2
for i in sorted(all_mounts):
if i not in not_okay:
print('OK - {}'.format(i))
exit(exitcode)