#!/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 os
import sys

import lib.args
import lib.base
import lib.disk
import lib.human
import lib.time
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Checks the status of the last borgbackup run by parsing the borg logfile.
Alerts on return codes of 2 or higher from the create or prune steps, and if the last
backup started longer ago than a configurable threshold (default: 24 hours), also while it
is still running. Also detects active borg mounts in /proc/mounts.
Requires root or sudo."""

DEFAULT_CRIT = None
DEFAULT_WARN = 24


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(
        '-c',
        '--critical',
        help='CRIT threshold for the time since the last backup started, in hours. '
        'Default: %(default)s',
        dest='CRIT',
        type=int,
        default=DEFAULT_CRIT,
    )

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

    parser.add_argument(
        '-w',
        '--warning',
        help='WARN threshold for the time since the last backup started, in hours. '
        'Default: %(default)s',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    args, _ = parser.parse_known_args()
    return args


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)

    # fetch data
    path = '/var/log/borg/borg.log'

    # content of the log file:
    # start: 2020-04-08 23:00:01
    # prune_retc: 0
    # create_retc: 0
    # end: 2020-04-08 23:00:13

    if os.path.exists(path) <= 0 and not lib.disk.file_exists(path, allow_empty=True):
        lib.base.cu(f'Logfile {path} not found or empty.')
    mounts = lib.base.coe(lib.disk.read_file('/proc/mounts'))
    log = lib.base.coe(lib.disk.read_file(path))

    # init some vars
    perfdata = ''
    starttime = None
    endtime = None
    create_retc = None
    prune_retc = None

    # analyze data
    mount_lines = ''
    for line in mounts.splitlines(keepends=True):
        if 'borgfs' in line:
            mount_lines += '* ' + line

    if mount_lines:
        lib.base.oao(
            f'There are active borg mounts.\n\n{mount_lines}',
            STATE_WARN,
            always_ok=args.ALWAYS_OK,
        )

    for line in log.splitlines():
        if line.startswith('start'):
            starttime = line.split(': ')[1].strip()

        if line.startswith('end'):
            endtime = line.split(': ')[1].strip()

        if line.startswith('create_retc'):
            create_retc = int(line.split(': ')[1])

        if line.startswith('prune_retc'):
            prune_retc = int(line.split(': ')[1])

    now = lib.time.now(as_type='iso')
    if starttime is not None and endtime is None:
        # The backup script empties the log and writes `start:` when a run begins, and
        # `end:` only when it is over, so this is a backup still running. One that has
        # been running for longer than the thresholds allow is reported like an old
        # backup, because it has not produced a new one either.
        delta = lib.time.timestrdiff(now, starttime)
        running_state = lib.base.get_state(
            delta,
            args.WARN * 60 * 60 if args.WARN is not None else None,
            args.CRIT * 60 * 60 if args.CRIT is not None else None,
        )
        lib.base.oao(
            f'Backup running since {starttime} ({lib.human.seconds2human(delta)})'
            f'{lib.base.state2str(running_state, prefix=" ")}.',
            running_state,
            always_ok=args.ALWAYS_OK,
        )

    if any(v is None for v in (starttime, endtime, create_retc, prune_retc)):
        lib.base.cu('Could not find all expected values in the logfile.')

    # We ignore retc 1, as it means operation reached its normal end,
    # but there were warnings from borg.
    state = STATE_OK
    create_retc_state = lib.base.get_state(create_retc, 2, None, _operator='ge')
    state = lib.base.get_worst(state, create_retc_state)
    perfdata += lib.base.get_perfdata(
        'create_retc',
        create_retc,
        warn=1,
        crit=2,
        _min=0,
    )

    prune_retc_state = lib.base.get_state(prune_retc, 2, None, _operator='ge')
    state = lib.base.get_worst(state, prune_retc_state)
    perfdata += lib.base.get_perfdata(
        'prune_retc',
        prune_retc,
        warn=1,
        crit=2,
        _min=0,
    )

    delta = lib.time.timestrdiff(now, starttime)

    last_starttime_state = lib.base.get_state(
        delta,
        args.WARN * 60 * 60 if args.WARN is not None else None,
        args.CRIT * 60 * 60 if args.CRIT is not None else None,
    )
    state = lib.base.get_worst(state, last_starttime_state)

    duration = lib.time.timestrdiff(endtime, starttime)  # in seconds
    perfdata += lib.base.get_perfdata(
        'duration',
        duration,
        uom='s',
        _min=0,
    )

    # build the message
    msg = (
        f'Last Backup started {starttime}, ended {endtime},'
        f' took {lib.human.seconds2human(duration)}.\n'
    )
    if state != STATE_OK:
        if last_starttime_state != STATE_OK:
            msg = 'Last backup is too long ago. ' + msg
        else:
            msg = 'One or more errors. ' + msg
    msg += (
        f'* Create retc: {create_retc}, '
        f'State: {lib.base.state2str(create_retc_state, empty_ok=False)}\n'
    )
    msg += (
        f'* Prune retc: {prune_retc}, '
        f'State: {lib.base.state2str(prune_retc_state, empty_ok=False)}'
    )

    # 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()
