#!/usr/lib64/linuxfabrik-monitoring-plugins/venv/bin/python
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details."""

import argparse
import json
import re
import sys
from collections import namedtuple

import lib.args
import lib.base
import lib.human
import lib.lftest
import lib.txt
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN

try:
    import psutil
except ImportError:
    print('Python module "psutil" is not installed.')
    sys.exit(STATE_UNKNOWN)

# psutil 5.x exposes sdiskpart/sdiskusage under psutil._common,
# psutil 6+ under psutil._ntuples. Fall back to plain namedtuples
# with the fields we touch if neither is available.
try:
    from psutil._ntuples import sdiskpart, sdiskusage
except ImportError:
    try:
        from psutil._common import sdiskpart, sdiskusage
    except ImportError:
        sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts'])
        sdiskusage = namedtuple('sdiskusage', ['total', 'used', 'free', 'percent'])


__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026081001'

DESCRIPTION = """Checks used or free disk space for each mounted partition. By default, only physical
devices are checked (hard disks, USB drives), ignoring pseudo and memory filesystems.
Supports filtering by mountpoint pattern or filesystem type. Thresholds can be set as
percentages or absolute values, and can target either used or free space, globally or
per mountpoint via --mount. On systems with many filesystems (hundreds of mounts),
--brief hides rows that are within the thresholds so the table only shows the
filesystems in WARN/CRIT state. Note that on ext2/3/4 filesystems, about 5% of disk
space is reserved for root by default and is not reflected in the available space shown
to regular users.
Alerts when usage exceeds the configured thresholds."""

DEFAULT_WARN = '90%USED'
DEFAULT_CRIT = '95%USED'
DEFAULT_NO_MATCH_SEVERITY = 'ok'


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=lib.args.epilog(__file__),
        formatter_class=lib.args.HelpFormatter,
    )

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version=f'%(prog)s: v{__version__} by {__author__}',
    )

    parser.add_argument(
        '--always-ok',
        help=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--brief',
        help='Hide table rows for filesystems within the thresholds and show only '
        'those in WARN/CRIT state. Perfdata and alerting are unaffected: all '
        'filesystems still emit perfdata and still drive the overall check state. '
        'Default: %(default)s',
        dest='BRIEF',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '-c',
        '--critical',
        help='CRIT threshold in the form `<number>[unit][method]`. '
        'Unit is one of `%%|K|M|G|T|P` (default: `%%`). `K` means kibibyte etc. '
        'Method is one of `USED|FREE` (default: `USED`). '
        '`USED` means "number or more", `FREE` means "number or less". '
        'Examples: `95` = 95%% used. `9.5M` = 9.5 MiB used. `5%%FREE`. `1400GUSED`. '
        'Default: %(default)s',
        dest='CRIT',
        type=lib.args.number_unit_method,
        default=DEFAULT_CRIT,
    )

    # Deprecated filter aliases, superseded by --match / --ignore. Kept
    # (hidden) so existing service definitions keep working: --include-*
    # feed the same list as --match, --exclude-* the same list as --ignore.
    parser.add_argument(
        '--exclude-pattern',
        help=argparse.SUPPRESS,
        dest='EXCLUDE_PATTERN',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--exclude-regex',
        help=argparse.SUPPRESS,
        dest='EXCLUDE_REGEX',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--fstype',
        help='Override the default behaviour (check physical devices only) and check these '
        'file system types instead. '
        'Can be specified multiple times. '
        'Run `disk-usage --list-fstypes` first to see available types (they are machine dependent).',
        dest='FSTYPE',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--ignore',
        help='Ignore mountpoints matching this Python regular expression. '
        'Case-insensitive by default (on Windows, drive letters and paths are '
        'case-insensitive; use drive letters such as `C:` or `C`). '
        'For case-sensitive matching, wrap the pattern in `(?-i:...)`, e.g. `(?-i:Data)`. '
        'Can be specified multiple times.',
        dest='IGNORE',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--include-pattern',
        help=argparse.SUPPRESS,
        dest='INCLUDE_PATTERN',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--include-regex',
        help=argparse.SUPPRESS,
        dest='INCLUDE_REGEX',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--list-fstypes',
        help='Print available file system types and which ones are checked by default, then exit.',
        dest='LIST_FSTYPES',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--match',
        help='Only check mountpoints matching this Python regular expression. '
        'Case-insensitive by default (on Windows, drive letters and paths are '
        'case-insensitive; use drive letters such as `C:` or `C`). '
        'For case-sensitive matching, wrap the pattern in `(?-i:...)`, e.g. `(?-i:Data)`. '
        'Can be specified multiple times. ' + lib.args.MATCH_IGNORE_PRECEDENCE,
        dest='MATCH',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--mount',
        help='Override the global --warning/--critical thresholds for a single mountpoint, '
        'in the form `<mountpoint>,<warning>,<critical>`. '
        'Each threshold uses the same `<number>[unit][method]` syntax as --warning/--critical. '
        'The mountpoint is matched exactly and case-insensitively, so the override always '
        'hits exactly one mountpoint and never several. '
        'On Windows, use drive letters such as `C:` or `C`. '
        'Can be specified multiple times. '
        'Example: `--mount=/var/log,80%%USED,90%%USED`',
        dest='MOUNT',
        type=lib.args.csv,
        action='append',
        default=None,
    )

    parser.add_argument(
        '--no-match-severity',
        help=lib.args.help('--no-match-severity') + ' Default: %(default)s',
        dest='NO_MATCH_SEVERITY',
        choices=['ok', 'warn', 'crit', 'unknown'],
        default=DEFAULT_NO_MATCH_SEVERITY,
    )

    parser.add_argument(
        '--no-perfdata',
        help=lib.args.help('--no-perfdata'),
        dest='NO_PERFDATA',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--perfdata-regex',
        help='Only emit perfdata keys matching this Python regex. '
        'For a list of perfdata keys, see the README or run this plugin. '
        'Can be specified multiple times.',
        action='append',
        dest='PERFDATA_REGEX',
        default=None,
    )

    parser.add_argument(
        '--test',
        help=lib.args.help('--test'),
        dest='TEST',
        type=lib.args.csv,
    )

    parser.add_argument(
        '-w',
        '--warning',
        help='WARN threshold in the form `<number>[unit][method]`. '
        'Unit is one of `%%|K|M|G|T|P` (default: `%%`). `K` means kibibyte etc. '
        'Method is one of `USED|FREE` (default: `USED`). '
        '`USED` means "number or more", `FREE` means "number or less". '
        'Examples: `95` = 95%% used. `9.5M` = 9.5 MiB used. `5%%FREE`. `1400GUSED`. '
        'Default: %(default)s',
        dest='WARN',
        type=lib.args.number_unit_method,
        default=DEFAULT_WARN,
    )

    args, _ = parser.parse_known_args()
    return args


def _load_disk_usage_fixture(raw_json):
    """Convert a test fixture into the two data structures the plugin
    expects from `psutil.disk_partitions()` and
    `psutil.disk_usage(mountpoint)`:

    - `partitions`: a list of `sdiskpart(device, mountpoint, fstype, opts)`
      namedtuples
    - `usage`: a dict keyed by mountpoint, each value an
      `sdiskusage(total, used, free, percent)` namedtuple

    Fixture shape:

        {
          "partitions": [
            {"device": "/dev/vda1", "mountpoint": "/", "fstype": "ext4",
             "opts": "rw,relatime"},
            ...
          ],
          "usage": {
            "/": {"total": <bytes>, "used": <bytes>, "free": <bytes>,
                  "percent": <0..100>},
            ...
          }
        }
    """
    data = json.loads(raw_json)
    partitions = [
        sdiskpart(
            p['device'],
            p['mountpoint'],
            p.get('fstype', ''),
            p.get('opts', ''),
        )
        for p in data.get('partitions', [])
    ]
    usage = {
        mp: sdiskusage(u['total'], u['used'], u['free'], u['percent'])
        for mp, u in data.get('usage', {}).items()
    }
    return partitions, usage


def list_fstypes():
    """Print a nice table showing which file system types exist and which are checked by default."""
    # get all partitions, no matter which ones
    parts = psutil.disk_partitions(all=True)
    table_data = []
    for part in parts:
        table_data.append(
            {
                'fstype': part.fstype,
                'mountpoint': part.mountpoint,
                'device': part.device,
                'checked': False,
            }
        )

    # get which ones are checked by default
    try:
        parts = psutil.disk_partitions(all=False)
    except AttributeError:
        pass
    for i, item in enumerate(table_data):
        for part in parts:
            if part.mountpoint == item['mountpoint']:
                table_data[i]['checked'] = True
                continue

    # sort table by fstype, mountpoint
    keys = ['fstype', 'mountpoint', 'device', 'checked']
    lib.base.oao(
        lib.base.get_table(table_data, keys, header=keys, sort_by_key='fstype'),
        STATE_OK,
    )


def normalize_mountpoint(mountpoint):
    """Normalize a mountpoint for exact, case-insensitive matching. Lowercases,
    turns backslashes into slashes, expands a bare Windows drive letter
    (`c` -> `c:`), and strips a trailing slash (except for root `/`), so that
    `C`, `C:` and `C:\\` all match the mountpoint `C:\\`.
    """
    mp = mountpoint.strip().lower().replace('\\', '/')
    if len(mp) == 1 and mp.isalpha():
        mp = f'{mp}:'
    if len(mp) > 1:
        mp = mp.rstrip('/')
    return mp


def _threshold_operands(usage, threshold):
    """Map a `(number, unit, method)` threshold triple (as produced by
    `lib.args.number_unit_method`) and a filesystem's usage onto the
    `(value, limit, operator)` arguments for `lib.base.get_state()`:

    - `USED` compares the used amount and alerts when it is "number or more"
      (`ge`); `FREE` compares the free amount and alerts when it is "number or
      less" (`le`).
    - a `%` unit compares percentages, any other unit compares absolute bytes.
    """
    number, unit, method = threshold
    if unit == '%':
        value = usage.percent if method == 'USED' else 100.0 - usage.percent
        limit = number
    else:
        value = usage.used if method == 'USED' else usage.free
        limit = lib.human.human2bytes(''.join(threshold[:2]))
    operator = 'ge' if method == 'USED' else 'le'
    return value, limit, operator


def _perfdata_thresholds(usage, threshold):
    """Map a `(number, unit, method)` threshold triple onto the numeric warn or
    crit line for the two threshold-bearing perfdata fields, mirroring how
    `evaluate_disk_state()` compares. Returns `(percent_limit, usage_limit)`,
    where each is the value to write into the respective perfdata field or
    `None` when the threshold does not apply to that field:

    - a `%` threshold drives the `-percent` field (used %); `USED` puts the
      line at `number`, `FREE` at `100 - number` (the used-% at which free hits
      the limit).
    - an absolute (byte) threshold drives the `-usage` field (used bytes);
      `USED` puts the line at `number` bytes, `FREE` at `total - number` bytes.
    """
    number, unit, method = threshold
    if unit == '%':
        percent_limit = float(number) if method == 'USED' else 100.0 - float(number)
        return percent_limit, None
    limit_bytes = lib.human.human2bytes(''.join(threshold[:2]))
    usage_limit = limit_bytes if method == 'USED' else usage.total - limit_bytes
    return None, usage_limit


def evaluate_disk_state(usage, warn, crit):
    """Return the WARN/CRIT state for one filesystem. `warn` and `crit` are
    `(number, unit, method)` triples as produced by
    `lib.args.number_unit_method`. Checks WARN first, then CRIT, and combines
    them with get_worst().
    """
    warn_value, warn_limit, warn_op = _threshold_operands(usage, warn)
    crit_value, crit_limit, crit_op = _threshold_operands(usage, crit)
    disk_state = lib.base.get_state(warn_value, warn_limit, None, warn_op)
    return lib.base.get_worst(
        disk_state,
        lib.base.get_state(crit_value, None, crit_limit, crit_op),
    )


def main():
    """The main function. This is where the magic happens."""

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    # set default values for append parameters that were not specified
    if args.EXCLUDE_PATTERN is None:
        args.EXCLUDE_PATTERN = []
    if args.EXCLUDE_REGEX is None:
        args.EXCLUDE_REGEX = []
    if args.FSTYPE is None:
        args.FSTYPE = []
    if args.IGNORE is None:
        args.IGNORE = []
    if args.INCLUDE_PATTERN is None:
        args.INCLUDE_PATTERN = []
    if args.INCLUDE_REGEX is None:
        args.INCLUDE_REGEX = []
    if args.MATCH is None:
        args.MATCH = []
    if args.MOUNT is None:
        args.MOUNT = []
    if args.PERFDATA_REGEX is None:
        args.PERFDATA_REGEX = []

    # args.WARN[0] = number, args.WARN[1] = unit, args.WARN[2] = USED|FREE
    try:
        float(args.WARN[0])
        float(args.CRIT[0])
    except ValueError:
        lib.base.cu('Invalid parameter value.')

    # --mount: per-mountpoint threshold overrides. Build a lookup keyed by the
    # normalized mountpoint; each value is a (warn, crit) pair of
    # number_unit_method triples that override the global thresholds for that
    # one mountpoint. mount_inputs keeps the mountpoint as the user typed it,
    # for the "matched no checked filesystem" note below.
    mount_overrides = {}
    mount_inputs = {}
    for entry in args.MOUNT:
        if len(entry) != 3:
            lib.base.cu(
                f'`--mount` needs `<mountpoint>,<warning>,<critical>`, got: {",".join(entry)}'
            )
        mountpoint, warn_raw, crit_raw = entry
        warn = lib.args.number_unit_method(warn_raw.strip())
        crit = lib.args.number_unit_method(crit_raw.strip())
        try:
            float(warn[0])
            float(crit[0])
        except ValueError:
            lib.base.cu(f'Invalid `--mount` threshold for "{mountpoint}".')
        mount_key = normalize_mountpoint(mountpoint)
        mount_overrides[mount_key] = (warn, crit)
        mount_inputs[mount_key] = mountpoint.strip()

    # show partition information and exit
    if args.LIST_FSTYPES:
        list_fstypes()

    # init some vars
    state = STATE_OK
    perfdata = ''
    table_data = []
    seen_overrides = set()  # which --mount keys actually matched a checked filesystem
    # --match / --ignore are the canonical filter names. Unlike the generic
    # case-sensitive --match/--ignore convention used elsewhere, disk-usage
    # filters case-insensitively because filesystem paths and Windows drive
    # letters are case-insensitive. The deprecated --include-regex /
    # --exclude-regex aliases feed the same lists.
    compiled_include_regex = [
        lib.base.coe(p)
        for p in lib.txt.compile_regex(
            args.INCLUDE_REGEX + args.MATCH, '--match', flags=re.IGNORECASE
        )
    ]
    compiled_exclude_regex = [
        lib.base.coe(p)
        for p in lib.txt.compile_regex(
            args.EXCLUDE_REGEX + args.IGNORE, '--ignore', flags=re.IGNORECASE
        )
    ]
    compiled_perfdata_regex = [
        lib.base.coe(p)
        for p in lib.txt.compile_regex(
            args.PERFDATA_REGEX, '--perfdata-regex', flags=re.IGNORECASE
        )
    ]

    # fetch data
    fixture_usage = None
    if args.TEST is None:
        if args.FSTYPE:
            # user wants to check file system types on his own
            parts = psutil.disk_partitions(all=True)
        else:
            # default behaviour - check physical devices only (e.g. hard disks, cd-rom drives,
            # USB keys) and ignore all others (e.g. pseudo, memory, duplicate, inaccessible
            # filesystems)
            try:
                parts = psutil.disk_partitions(all=False)
            except AttributeError:
                lib.base.cu(
                    'Did not find physical devices (e.g. hard disks, cd-rom drives, USB keys).'
                )
    else:
        stdout, _, _ = lib.lftest.test(args.TEST)
        parts, fixture_usage = _load_disk_usage_fixture(stdout)

    # analyze data
    for part in parts:
        # sdiskpart(device='/dev/vda2', mountpoint='/', fstype='ext4', opts='rw,relatime')
        # sdiskpart(
        #   device='/dev/sr0',
        #   mountpoint='/run/media/root/CentOS 7 x86_64',
        #   fstype='iso9660',
        #   opts='ro,nosuid,nodev,relatime,uid=0,gid=0,iocharset=utf8,mode=0400,dmode=0500'
        # )
        # ignore `/snap`, iso mountpoints and cdroms (UDF = universal disk format)
        if args.FSTYPE:
            # user wants to check file system types on his own
            if part.fstype not in args.FSTYPE:
                continue
        else:
            # default behaviour - ignore read-only and some other filesystems
            if part.fstype in ['CDFS', 'iso9660', 'squashfs', 'UDF'] or part.opts in [
                'cdrom'
            ]:
                continue

        # Filter mountpoints. --match (include) is applied first, then
        # --ignore (exclude), so a mountpoint hit by --ignore is dropped even
        # if it also matches --match. Matching is case-insensitive.
        # hint: we can't do `if not part or part.mountpoint in args.IGNORE:` because it is
        # impossible to specify a "Y:\" on the command line ('Y:\' or 'Y:\\' all don't work).
        # The regexes are matched against the mountpoint as-is: they already carry
        # re.IGNORECASE, and lowercasing the subject as well would defeat a
        # `(?-i:...)` group that opts back into case-sensitive matching. Only the
        # deprecated substring aliases lowercase both sides themselves.
        mountpoint = part.mountpoint
        mountpoint_lower = mountpoint.lower()
        if args.INCLUDE_PATTERN or args.INCLUDE_REGEX or args.MATCH:
            if not any(
                include_pattern.lower() in mountpoint_lower
                for include_pattern in args.INCLUDE_PATTERN
            ) and not any(item.search(mountpoint) for item in compiled_include_regex):
                continue
        if args.EXCLUDE_PATTERN or args.EXCLUDE_REGEX or args.IGNORE:
            if any(
                exclude_pattern.lower() in mountpoint_lower
                for exclude_pattern in args.EXCLUDE_PATTERN
            ) or any(item.search(mountpoint) for item in compiled_exclude_regex):
                continue

        try:
            if fixture_usage is not None:
                if part.mountpoint not in fixture_usage:
                    raise FileNotFoundError(part.mountpoint)
                usage = fixture_usage[part.mountpoint]
            else:
                usage = psutil.disk_usage(part.mountpoint)
        except (PermissionError, FileNotFoundError, OSError):
            table_data.append(
                {
                    'mountpoint': f'{part.mountpoint}',
                    'type': f'{part.fstype}',
                    'used': 'N/A',
                    'avail': 'N/A',
                    'size': 'N/A',
                    'percent': 'N/A',
                    # unreadable filesystems do not alert and sort to the
                    # bottom of the usage-sorted table (see get_table below).
                    '_state': STATE_OK,
                    '_override': False,
                    '_percent': -1.0,
                }
            )
            continue

        # evaluate WARN/CRIT, using a --mount per-mountpoint override if one
        # matches this mountpoint, otherwise the global thresholds
        mount_key = normalize_mountpoint(part.mountpoint)
        is_override = mount_key in mount_overrides
        if is_override:
            seen_overrides.add(mount_key)
        warn, crit = mount_overrides.get(mount_key, (args.WARN, args.CRIT))
        disk_state = evaluate_disk_state(usage, warn, crit)
        state = lib.base.get_worst(state, disk_state)

        # Map the effective thresholds onto the perfdata fields they apply to,
        # so graphing tools can draw warn/crit lines. A percentage threshold
        # lands on `-percent`, an absolute (byte) threshold on `-usage`; the
        # other field stays unthresholded.
        warn_percent, warn_usage = _perfdata_thresholds(usage, warn)
        crit_percent, crit_usage = _perfdata_thresholds(usage, crit)

        # Use% cell: the percentage, the per-mountpoint thresholds when --mount
        # overrides this row (so the table explains why an otherwise-fine row
        # alerts), and the state marker last (kept at the end of the cell for
        # IcingaWeb, which turns it into an icon).
        percent_cell = f'{usage.percent}%'
        if is_override:
            percent_cell += f' (warn={"".join(warn)} crit={"".join(crit)})'
        percent_cell += lib.base.state2str(disk_state, prefix=' ')

        perfdata_key = f'{part.mountpoint}-usage'
        if not args.PERFDATA_REGEX or any(
            item.search(perfdata_key) for item in compiled_perfdata_regex
        ):
            perfdata += lib.base.get_perfdata(
                perfdata_key,
                usage.used,
                uom='B',
                warn=warn_usage,
                crit=crit_usage,
                _min=0,
                _max=usage.total,
            )
        perfdata_key = f'{part.mountpoint}-total'
        if not args.PERFDATA_REGEX or any(
            item.search(perfdata_key) for item in compiled_perfdata_regex
        ):
            perfdata += lib.base.get_perfdata(
                perfdata_key,
                usage.total,
                uom='B',
                warn=None,
                crit=None,
                _min=0,
                _max=usage.total,
            )
        perfdata_key = f'{part.mountpoint}-percent'
        if not args.PERFDATA_REGEX or any(
            item.search(perfdata_key) for item in compiled_perfdata_regex
        ):
            perfdata += lib.base.get_perfdata(
                perfdata_key,
                usage.percent,
                uom='%',
                warn=warn_percent,
                crit=crit_percent,
                _min=0,
                _max=100,
            )
        table_data.append(
            {
                'mountpoint': f'{part.mountpoint}',
                'type': f'{part.fstype}',
                'used': lib.human.bytes2human(usage.used),
                'avail': lib.human.bytes2human(usage.free),
                'size': lib.human.bytes2human(usage.total),
                'percent': percent_cell,
                # internal per-row fields, not rendered because get_table()
                # uses an explicit column whitelist: per-row state for the
                # --brief filter below, whether a --mount override applied
                # (so the single-mountpoint summary does not repeat the
                # thresholds the Use% value already carries), and the numeric
                # usage percentage the table is sorted by.
                '_state': disk_state,
                '_override': is_override,
                '_percent': usage.percent,
            }
        )

    # Filter table rows for --brief display: hide rows within the
    # thresholds and keep only WARN/CRIT rows. Perfdata and alerting stay
    # untouched above this point; --brief only reshapes the human-readable
    # output.
    if args.BRIEF:
        display_rows = [
            row for row in table_data if row.get('_state', STATE_OK) != STATE_OK
        ]
    else:
        display_rows = table_data

    # build the message
    thresholds = f'warn={"".join(args.WARN)} crit={"".join(args.CRIT)}'
    if not table_data:
        msg = 'Nothing checked.'
        state = lib.base.str2state(args.NO_MATCH_SEVERITY)
    elif args.BRIEF and not display_rows:
        # --brief and nothing worth showing: one-line summary only.
        msg = f'Everything is ok. ({thresholds})'
    elif len(table_data) == 1 and not args.BRIEF:
        # single matched filesystem (no --brief): full single-line summary with
        # the values inline. A --mount override already carries its thresholds
        # in the Use% value, so only append the global thresholds otherwise.
        row = table_data[0]
        msg = (
            f'{row["mountpoint"]}'
            f' {row["percent"]}'
            f' - total: {row["size"]}'
            f', free: {row["avail"]}'
            f', used: {row["used"]}'
        )
        if not row.get('_override'):
            msg += f' ({thresholds})'
    else:
        if state == STATE_CRIT:
            header = 'There are critical errors.'
        elif state == STATE_WARN:
            header = 'There are warnings.'
        else:
            header = 'Everything is ok.'
        table = lib.base.get_table(
            display_rows,
            ['mountpoint', 'type', 'size', 'used', 'avail', 'percent'],
            header=['Mountpoint', 'Type', 'Size', 'Used', 'Avail', 'Use%'],
            sort_by_key='_percent',
            sort_order_reverse=True,
        )
        msg = f'{header} ({thresholds})\n\n{table}'

    # warn (in the output, not in the state) about --mount entries that did not
    # match any checked filesystem, so a typo or a filesystem that is not
    # checked by default does not silently disable an intended threshold. The
    # note goes on the first line (not the long output) so it is visible in the
    # web UI and in notifications without expanding the output.
    unmatched = sorted(
        mount_inputs[key] for key in mount_overrides if key not in seen_overrides
    )
    if unmatched:
        note = 'ignored `--mount` for ' + ', '.join(unmatched) + ' (not checked)'
        head, separator, tail = msg.partition('\n')
        msg = f'{head} {note}{separator}{tail}'

    # over and out
    lib.base.oao(
        msg, state, perfdata, always_ok=args.ALWAYS_OK, no_perfdata=args.NO_PERFDATA
    )


if __name__ == '__main__':
    try:
        main()
    except Exception:
        lib.base.cu()
