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

import lib.args
import lib.base
import lib.lftest
import lib.qts
import lib.url
import lib.version
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

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


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

DESCRIPTION = """Checks if firmware updates are available for a QNAP appliance running QTS, as reported
by the update check of the appliance itself. Compares version and build number, so a new
build of the installed version counts as an update.
Alerts when a firmware update is available."""

DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 6
DEFAULT_USERNAME = 'admin'


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(
        '--insecure',
        help=lib.args.help('--insecure'),
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--no-proxy',
        help=lib.args.help('--no-proxy'),
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '--password',
        help='QTS API password.',
        dest='PASSWORD',
        required=True,
    )

    parser.add_argument(
        '--proxy',
        help=lib.args.help('--proxy'),
        dest='PROXY',
        default=None,
    )

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

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

    parser.add_argument(
        '--url',
        help='QTS-based appliance URL. Example: `--url=https://192.0.2.10:8080`.',
        dest='URL',
        required=True,
    )

    parser.add_argument(
        '--username',
        help='QTS API username. Default: %(default)s',
        dest='USERNAME',
        default=DEFAULT_USERNAME,
    )

    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
    if args.TEST is None:
        auth_sid = lib.base.coe(lib.qts.get_auth_sid(args))
        url = (
            f'{args.URL}/cgi-bin/management/manaRequest.cgi'
            f'?subfunc=sysinfo&hd=no&multicpu=1&sid={auth_sid}'
        )
        sysinfo = lib.base.coe(
            lib.url.fetch(
                url,
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                proxy=args.PROXY,
                timeout=args.TIMEOUT,
            )
        )
        data = xmltodict.parse(sysinfo)['QDocRoot']
        if data['authPassed'] == '0':
            lib.base.cu('Insufficient permissions.')
        url = (
            f'{args.URL}/cgi-bin/sys/sysRequest.cgi?subfunc=firm_update&sid={auth_sid}'
        )
        firm_update = lib.base.coe(
            lib.url.fetch(
                url,
                data=data,
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                proxy=args.PROXY,
                timeout=args.TIMEOUT,
            )
        )
    else:
        # do not call the appliance, put in test data
        base = args.TEST[0]
        data = xmltodict.parse(lib.lftest.test_text(args.TEST, f'{base}-sysinfo'))[
            'QDocRoot'
        ]
        if data['authPassed'] == '0':
            lib.base.cu('Insufficient permissions.')
        firm_update = lib.lftest.test_text(args.TEST, f'{base}-firm-update')

    # init some vars
    installed_version = data['firmware']['version']
    # QTS 4.2 does not report a number (python-qnapstats fixtures of a TS-410)
    installed_number = data['firmware'].get('number')
    if installed_number:
        installed_version = f'{installed_version}.{installed_number}'
    installed = f'{installed_version} Build {data["firmware"]["build"]}'

    # analyze data
    # The available firmware comes as "<version>.<number> Build <date>", for example
    # "c5.2.9.3468 Build 20260413", and as "none" or "error" otherwise. Up to
    # QuTScloud c5.0.1 there is one `newVersion` right in `ownContent`, from c5.1.0 on
    # there is one per update channel in `ownContent/official` and
    # `ownContent/recommended` (verified against QuTScloud c4.5.7.1864, c5.0.1.1949,
    # c5.1.0.2498, c5.2.4.3041 and c5.2.9.3468). A channel reports "error" when QNAP
    # does not serve its feed: QuTScloud c5.1 asks for
    # `QuTScloud_FW_5.1.0_recommended.xml`, which answers HTTP 403, so an error of the
    # recommended channel alone is not worth an alert. Where no channel names a
    # version, the appliance knows of no update: the `firmware` block of the same
    # answer describes the installed firmware, not an available one.
    func = xmltodict.parse(firm_update)['QDocRoot'].get('func') or {}
    own_content = func.get('ownContent') or {}
    main_channel = own_content.get('newVersion') or (
        own_content.get('official') or {}
    ).get('newVersion')
    new_versions = [
        main_channel,
        (own_content.get('recommended') or {}).get('newVersion'),
    ]

    # Compare version and number. The build date is left out, and so is everything
    # from " Build" on, because lib.version.version() would glue the date to the
    # number. Four positions, because QNAP ships new numbers under the same version
    # (5.2.10.3568 and 5.2.10.3577).
    installed_tuple = lib.version.version(installed_version, maxlen=4)
    available = sorted(
        (lib.version.version(item.split()[0], maxlen=4), item)
        for item in new_versions
        if item and item not in ('error', 'none')
    )
    if not available and main_channel == 'error':
        lib.base.cu('Got an error from the QNAP API.')
    if not available or installed_tuple >= available[-1][0]:
        lib.base.oao(f'QTS v{installed} is up to date', STATE_OK)
    latest_version = available[-1][1]

    # build the message
    msg = (
        f'QTS v{installed} installed, QTS v{latest_version} available'
        f'{lib.base.state2str(STATE_WARN, prefix=" ")}'
    )

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


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