2020-11-09 19:31:06 +00:00
|
|
|
#!/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(
|
2021-04-23 12:01:24 +00:00
|
|
|
"systemctl show -p FragmentPath,Options,SourcePath,Type,UnitFileState,Where -- '{}'".format(mountunit),
|
2020-11-09 19:31:06 +00:00
|
|
|
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)
|