#!/usr/bin/env python3
# -*- 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 sys
import tempfile

import lib.args
import lib.base
import lib.task
from lib.globals import STATE_OK, STATE_UNKNOWN

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

DESCRIPTION = """Tests if a path is writable and readable by creating, writing, reading, and deleting
a temporary file. Especially useful for mounted filesystems such as NFS or SMB where
the mount may silently become read-only or unresponsive. The local temporary directory
is always tested as a baseline.
A path on a network filesystem whose server has stopped answering does not fail, it
blocks, and it blocks in a way no timeout inside a process reaches. Each path is
therefore tested in a process of its own, all of them at the same time and under one
shared deadline, so the check stays within --timeout however many paths are unreachable,
and a path that misses the deadline is reported like one that cannot be written to.
Alerts if the path is not writable, not readable, or does not answer at all.
Requires root or sudo."""

DEFAULT_PATH = [tempfile.gettempdir()]
DEFAULT_SEVERITY = 'warn'
DEFAULT_TIMEOUT = 8


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(
        '--path',
        help='Path to test for read/write access by creating and deleting a temporary file. '
        'Can be specified multiple times. '
        'Default: %(default)s',
        dest='PATH',
        default=DEFAULT_PATH,
        action='append',
    )

    parser.add_argument(
        '--severity',
        help=lib.args.help('--severity') + ' Default: %(default)s',
        dest='SEVERITY',
        default=DEFAULT_SEVERITY,
        choices=['warn', 'crit'],
    )

    parser.add_argument(
        '--timeout',
        help='How long a path gets to answer before it is reported as unreachable. '
        'Every path is tested at the same time and they share one deadline, so this is '
        'the runtime of the whole check and not a budget per path. '
        'Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    args, _ = parser.parse_known_args()
    return args


def test_path(path):
    """Create a temporary file below `path`, write to it, read it back and remove it
    again. Raises where any of that does not work.

    Runs in a process of its own, so it says nothing and returns nothing: whether it
    worked is the whole answer, and an exception arrives at the caller as its message.
    """
    with tempfile.TemporaryFile(dir=path) as fp:
        # a bytes-like object is required, not 'str':
        fp.write(b'Linuxfabrik GmbH, Zurich, Switzerland')
        fp.seek(0)
        fp.read()


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)

    # A path named twice, which is what naming the temporary directory explicitly does
    # next to the default, is tested and reported once.
    paths = list(dict.fromkeys(args.PATH))

    # fetch data
    # Every path is tested at the same time under one shared deadline, so a path whose
    # server has gone away costs the check its --timeout once instead of once per path.
    answers = lib.task.run_each(
        [(path, lambda path=path: test_path(path)) for path in paths],
        args.TIMEOUT,
    )

    # init some vars
    msg = ''
    state = STATE_OK

    # analyze data
    for path in paths:
        success, answer = answers[path]
        if success:
            continue
        if answer == lib.task.TIMEOUT:
            # the path did not fail, nothing came back from it at all
            answer = f'no answer within {args.TIMEOUT}s'
        msg += f'`{path}` ({answer}), '
        state = lib.base.str2state(args.SEVERITY)

    # build the message
    if msg == '':
        msg = 'Everything is ok. '
    else:
        msg = f'Error creating/writing/reading/deleting file in {msg[:-2]}. '
    msg += f'Tested: {", ".join(paths)}'

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


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