#!/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.time  # 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 plugin retrieves sensor information for each device
                 from a LibreNMS instance.

                 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_INSECURE = False
DEFAULT_LENGTHY = False
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 3
DEFAULT_URL = 'http://localhost'


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

    parser.add_argument(
        '-V', '--version',
        action='version',
        version='%(prog)s: v{} by {}'.format(__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(
        '--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
    state = STATE_OK
    perfdata = ''
    alert_count = 0
    sensors_count = 0

    # fetch data
    sql = '''
        SELECT 
            d.hostname,
            d.sysName,
            d.sysDescr,
            d.type,
            l.location,
            s.sensor_descr,
            s.sensor_current,
            s.sensor_limit,
            s.sensor_limit_low,
            s.lastupdate,
            s.sensor_class,
            st.state_descr,
            st.state_generic_value
        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
            sensors AS s ON d.device_id = s.device_id
                LEFT JOIN
            sensors_to_state_indexes AS stsi ON s.sensor_id = stsi.sensor_id
                LEFT JOIN
            state_translations AS st ON stsi.state_index_id = st.state_index_id
                AND st.state_value = s.sensor_current
        WHERE
            disable_notify = 0
            AND s.sensor_alert = 1
    '''
    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
    sql += ' ORDER BY d.hostname, s.sensor_class'
    sensors = lib.base.coe(lib.db_mysql.select(conn, sql, data))
    lib.db_mysql.close(conn)
    sensors_count = len(sensors)

    # enrich and analyse data
    for i, sensor in enumerate(sensors):
        if not sensor['sysName']:
            sensors[i]['sysName'] = sensor['sysDescr']
        if sensor['sensor_class'] == 'state':
            sensors[i]['sensor_current'] = sensor['state_descr']
        if all([sensor['sensor_limit_low'], sensor['sensor_limit']]):
            sensor['sensor_current'] = '{} ({}..{})'.format(
                sensor['sensor_current'],
                sensor['sensor_limit_low'],
                sensor['sensor_limit'],
            )
        if sensor['lastupdate']:
            delta = lib.time.now(as_type='datetime') - sensor['lastupdate']
            sensor['lastupdate'] = lib.human.seconds2human(delta.total_seconds())
        local_state = sensor['state_generic_value'] # 0 = ok, 1 = warn, 2 = crit, 3 = unknown
        if not local_state: # including "None"
            local_state = STATE_OK
        if local_state != STATE_OK:
            alert_count += 1
        sensors[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
        sensors = [sensor for sensor in sensors if sensor['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(
        sensors_count,
        lib.txt.pluralize('sensor', sensors_count),
    )
    msg += '\n\n'

    perfdata += lib.base.get_perfdata('sensor_count', sensors_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 sensors_count > 0:
        if not args.LENGTHY:
            msg += lib.base.get_table(
                sensors,
                [
                    'hostname',
                    'sysName',
                    'sensor_descr',
                    'sensor_current',
                    'state',
                ],
                header=[
                    'Hostname',
                    'SysName',
                    'Sensor',
                    'Val (Range)',
                    'State',
                ],
            )
        else:
            msg += lib.base.get_table(
                sensors,
                [
                    'hostname',
                    'sysName',
                    'type',
                    'location',
                    'sensor_descr',
                    'sensor_class',
                    'lastupdate',
                    'sensor_current',
                    'state',
                ],
                header=[
                    'Hostname',
                    'SysName',
                    'Type',
                    'Location',
                    'Sensor',
                    'Class',
                    'Changed',
                    'Val (Range)',
                    '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()
