#!/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)