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