#!/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  # pylint: disable=C0413
import sys  # pylint: disable=C0413

import lib.base  # pylint: disable=C0413
import lib.db_mysql  # pylint: disable=C0413
import lib.human  # pylint: disable=C0413
import lib.librenms  # pylint: disable=C0413
import lib.txt  # pylint: disable=C0413
from lib.globals import (STATE_CRIT, STATE_OK,  # pylint: disable=C0413
                          STATE_UNKNOWN, STATE_WARN)

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

DESCRIPTION = """This check warns of unacknowledged alerts in LibreNMS and
                 reports the most recent alert for each device (only for those
                 that do not have "Disabled alerting" in their LibreNMS device
                 settings). If alerts have been triggered in LibreNMS, you will
                 see them on the *Alerts > Notifications* page within the
                 Web UI. When you acknowledge an alert in LibreNMS, this check
                 will change the status for the corresponding device to OK.

                 This check requires direct access to the LibreNMS MySQL/MariaDB
                 database, because the API is simply too resource intensive for
                 use in a large scale environment."""

DEFAULT_DEFAULTS_FILE = '/var/spool/icinga2/.my.cnf'
DEFAULT_DEFAULTS_GROUP = 'client'
DEFAULT_LENGTHY = False
DEFAULT_SERVERITY = 'crit'
DEFAULT_TIMEOUT = 3


def parse_args():
    """Parse command line arguments using argparse.
    """
    parser = argparse.ArgumentParser(description=DESCRIPTION)

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

    parser.add_argument(
        '--always-ok',
        help='Always returns OK.',
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--defaults-file',
        help='Specifies a cnf file to read parameters like user, host and '
             'password from (instead of specifying them on the command line), '
             'for example `/var/spool/icinga2/.my.cnf`. '
             'Default: %(default)s',
        dest='DEFAULTS_FILE',
        default=DEFAULT_DEFAULTS_FILE,
    )

    parser.add_argument(
        '--defaults-group',
        help='Group/section to read from in the cnf file. '
             'Default: %(default)s',
        dest='DEFAULTS_GROUP',
        default=DEFAULT_DEFAULTS_GROUP,
    )

    parser.add_argument(
        '--device-group',
        help='Filter by LibreNMS Device Group. Supports SQL Wildcards.',
        dest='DEVICE_GROUP',
    )

    parser.add_argument(
        '--device-hostname',
        help='Filter by LibreNMS Hostname (repeating).',
        dest='DEVICE_HOSTNAME',
        action='append',
    )

    parser.add_argument(
        '--device-type',
        help='Filter by LibreNMS Device Type (repeating).',
        dest='DEVICE_TYPE',
        action='append',
        choices=[ # taken from the librenms source file misc/config_definitions.json
            'appliance',
            'collaboration',
            'environment',
            'firewall',
            'loadbalancer',
            'network',
            'power',
            'printer',
            'server',
            'storage',
            'wireless',
            'workstation',
        ],
    )

    parser.add_argument(
        '--lengthy',
        help='Extended reporting. ',
        dest='LENGTHY',
        action='store_true',
        default=DEFAULT_LENGTHY,
    )

    parser.add_argument(
        '--severity',
        help='Severity for alerts. One of "warn" or "crit". '
             'Default: %(default)s',
        dest='SEVERITY',
        default=DEFAULT_SERVERITY,
        choices=['warn', 'crit'],
    )

    parser.add_argument(
        '--timeout',
        help='Network timeout in seconds. '
             'Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    return parser.parse_args()


def main():
    """The main function. Hier spielt die Musik.
    """

    # parse the command line, exit with UNKNOWN if it fails
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    mysql_connection = {
        'defaults_file':  args.DEFAULTS_FILE,
        'defaults_group': args.DEFAULTS_GROUP,
        'timeout':        args.TIMEOUT,
    }
    conn = lib.base.coe(lib.db_mysql.connect(mysql_connection))
    lib.base.coe(lib.db_mysql.check_select_privileges(conn))

    # init some vars
    msg = ''
    perfdata = ''
    state = STATE_OK
    alert_count = 0

    # fetch data
    sql = '''
        SELECT 
            d.hardware,
            d.hostname,
            d.os,
            d.sysName,
            d.sysDescr,
            d.type,
            d.uptime,
            l.location,
            a.state,
            ar.name AS ar_name
        FROM
            devices AS d
                LEFT JOIN
            locations AS l ON d.location_id = l.id
                LEFT JOIN
            device_group_device AS dgd ON d.device_id = dgd.device_id
                LEFT JOIN
            device_groups AS dg ON dgd.device_group_id = dg.id
                LEFT JOIN
            alerts AS a ON d.device_id = a.device_id
                AND a.state = 1
                LEFT JOIN
            alert_rules AS ar ON ar.id = a.rule_id
        WHERE
            disable_notify = 0
    '''
    data = []
    if args.DEVICE_GROUP:
        sql += ' AND dg.name LIKE %s '
        data.append(args.DEVICE_GROUP)
    if args.DEVICE_HOSTNAME:
        sql += ' AND d.hostname IN ({}) '.format(
            ', '.join('%s' for d in args.DEVICE_HOSTNAME), # print "%s" for each argument
        )
        data += args.DEVICE_HOSTNAME
    if args.DEVICE_TYPE:
        sql += ' AND d.type IN ({}) '.format(
            ', '.join('%s' for d in args.DEVICE_TYPE), # print "%s" for each argument
        )
        data += args.DEVICE_TYPE
    devices = lib.base.coe(lib.db_mysql.select(conn, sql, data))
    lib.db_mysql.close(conn)
    device_count = len(devices)

    # enrich and analyse data
    for i, device in enumerate(devices):
        if device['uptime']:
            devices[i]['uptime'] = lib.human.seconds2human(device['uptime'])
        if not device['sysName']:
            devices[i]['sysName'] = device['sysDescr']
        local_state = lib.librenms.get_state(device['state'], args.SEVERITY)
        if local_state != STATE_OK:
            alert_count += 1
        devices[i]['state'] = lib.base.state2str(
            local_state,
            empty_ok=False,
        )
        state = lib.base.get_worst(local_state, state)

    # filter data if compact layout is choosen (just get everything that is not ok)
    if not args.LENGTHY:
        # brief data
        devices = [device for device in devices if device['state'] != '[OK]']

    # build the message
    if state == STATE_OK:
        msg = 'Everything is ok. '
    else:
        msg = 'There {} {} {}. '.format(
            lib.txt.pluralize('', alert_count, 'is,are'),
            alert_count,
            lib.txt.pluralize('alert', alert_count),
        )
    msg += 'Checked {} {}.'.format(
        device_count,
        lib.txt.pluralize('device', device_count),
    )
    msg += '\n\n'

    perfdata += lib.base.get_perfdata('device_count', device_count, None, None, None, 0, None) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('alert_count', alert_count, None, None, None, 0, None) # pylint: disable=C0301

    if device_count > 0:
        if not args.LENGTHY:
            msg += lib.base.get_table(
                devices,
                [
                    'hostname',
                    'sysName',
                    'ar_name',
                    'state',
                ],
                header=[
                    'Hostname',
                    'SysName',
                    'Alert',
                    'State',
                ],
            )
        else:
            msg += lib.base.get_table(
                devices,
                [
                    'hostname',
                    'sysName',
                    'hardware',
                    'type',
                    'os',
                    'location',
                    'uptime',
                    'ar_name',
                    'state',
                ],
                header=[
                    'Hostname',
                    'SysName',
                    'Hardware',
                    'Type',
                    'OS',
                    'Location',
                    'Uptime',
                    'Alert',
                    'State',
                ],
            )

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


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