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

import lib.args
import lib.base
import lib.disk
import lib.lftest
import lib.redfish
import lib.txt
from lib.globals import STATE_CRIT, STATE_OK, STATE_UNKNOWN, STATE_WARN

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

DESCRIPTION = """Checks the overall system health reported by a Redfish-compatible server via the
Redfish API. Reports every enabled system member with its identification (manufacturer, model,
hostname, SKU, serial number), compute summary (processors, BIOS version, power state,
indicator LED) and rolled-up health status, and alerts whenever any system's status leaves
`OK`. Use `redfish-storage` for drive- and storage-controller-specific monitoring."""

API_BASE = '/redfish/v1'
DEFAULT_CACHE_EXPIRE = (
    5  # minutes; also caches API responses, kept below the session timeout
)
DEFAULT_INSECURE = True
DEFAULT_NO_PROXY = False
DEFAULT_RETRIES = 3  # extra attempts on a failed Redfish request
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(
        '--cache-expire',
        help=lib.args.help('--cache-expire') + ' Default: %(default)s',
        dest='CACHE_EXPIRE',
        type=int,
        default=DEFAULT_CACHE_EXPIRE,
    )

    parser.add_argument(
        '--insecure',
        help=lib.args.help('--insecure'),
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--inventory',
        help='Output the parsed components as JSON on stdout and exit OK, instead of '
        'running a health check. Use this to collect a hardware inventory: the JSON is a '
        'single object keyed by component type, so the output of several Redfish checks can '
        'be merged into one inventory document with `jq --slurp`. Default: %(default)s',
        dest='INVENTORY',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--no-insecure',
        help=lib.args.help('--no-insecure'),
        dest='INSECURE',
        action='store_false',
        default=DEFAULT_INSECURE,
    )

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

    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='Redfish API password.',
        dest='PASSWORD',
    )

    parser.add_argument(
        '--retries',
        help='Number of extra attempts if a request to the Redfish API fails, before the '
        'check gives up. Helps against an occasionally slow or flaky management controller. '
        'Default: %(default)s',
        dest='RETRIES',
        type=int,
        default=DEFAULT_RETRIES,
    )

    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='Redfish API URL.',
        dest='URL',
        required=True,
    )

    parser.add_argument(
        '--username',
        help='Redfish API username.',
        dest='USERNAME',
    )

    args, _ = parser.parse_known_args()
    return args


def load_test_fixture(test_args, path):
    # Replace the first element of args.TEST with the walk-specific
    # fixture path, read it via lib.lftest.test() and return the parsed
    # JSON. On a missing file or malformed JSON, exit STATE_UNKNOWN with
    # a helpful message instead of letting json.loads raise a traceback.
    if not lib.disk.file_exists(path, allow_empty=True):
        lib.base.cu(f'Test fixture not found: "{path}".')
    test_args[0] = path
    stdout, _, _ = lib.lftest.test(test_args)
    try:
        return json.loads(stdout)
    except (json.JSONDecodeError, ValueError) as e:
        lib.base.cu(f'Test fixture "{path}" does not contain valid JSON: {e}')


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:
        if not args.URL.startswith('http'):
            lib.base.cu('--url parameter has to start with "http://" or https://".')
        header = {'Accept': 'application/json'}
        # cache_expire (seconds) enables the lib fetch layer's shared per-URL cache,
        # so sibling Redfish checks on this host share one session and each fetch.
        cache_expire = args.CACHE_EXPIRE * 60
        header.update(lib.redfish.get_auth_header(args, cache_expire=cache_expire))
        expand = lib.redfish.get_expand_suffix(
            args.URL,
            header=header,
            insecure=args.INSECURE,
            no_proxy=args.NO_PROXY,
            timeout=args.TIMEOUT,
            retries=args.RETRIES,
            cache_expire=cache_expire,
        )
        # Entry point: the Systems collection, read in one request via the Redfish
        # $expand query and cached by the lib fetch layer so the sibling Redfish
        # checks on this host reuse it within the cache window.
        systems_url = f'{args.URL}{API_BASE}/Systems'
        result = lib.base.coe(
            lib.redfish.fetch_collection(
                systems_url,
                expand=expand,
                header=header,
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                timeout=args.TIMEOUT,
                retries=args.RETRIES,
                cache_expire=cache_expire,
            )
        )
    else:
        # do not call the API, put in test data. Each API call in the
        # Redfish walk has an explicit fixture suffix, so the fixture
        # file names describe what they contain (systems, system).
        test_base = args.TEST[0]
        result = load_test_fixture(args.TEST, f'{test_base}-systems')
    # "Members": [
    #     {
    #         "@odata.id": "/redfish/v1/Systems/437XR1138R2"
    #     }
    # ],
    if len(result.get('Members', [])) == 0:
        lib.base.cu('Nothing to check, no Redfish members found.')

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''
    member_count = 0
    # only allocated and populated in --inventory mode, so a normal health
    # check holds nothing extra in memory
    inventory = [] if args.INVENTORY else None

    # analyze data: follow each "Member" link and aggregate the rolled-up
    # Redfish system `Status` into `state`. Drive and storage-controller
    # health is covered by `redfish-storage`.
    # fetch_members fills in any systems the controller left as bare references
    # (i.e. when it did not honour $expand on the collection above)
    if args.TEST is None:
        system_members = lib.base.coe(
            lib.redfish.fetch_members(
                result.get('Members', []),
                args.URL,
                header=header,
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                timeout=args.TIMEOUT,
                retries=args.RETRIES,
                cache_expire=cache_expire,
            )
        )
    else:
        system_members = [
            load_test_fixture(args.TEST, f'{test_base}-system')
            for _ in range(len(result.get('Members', [])))
        ]
    for systems in system_members:
        systems = lib.redfish.get_systems(systems)
        if systems['Status_State'] not in ['Enabled', 'Quiesced']:
            continue
        member_count += 1
        # collect for --inventory
        if args.INVENTORY:
            inventory.append(dict(systems))
        systems_state = lib.redfish.get_state(systems)
        state = lib.base.get_worst(state, systems_state)

        # build the message
        msg += 'Member:'
        msg += f' {systems["Manufacturer"]}' if systems['Manufacturer'] else ''
        msg += f' {systems["Model"]}' if systems['Model'] else ''
        msg += ', '
        msg += f'HostName: {systems["HostName"]}, ' if systems['HostName'] else ''
        msg += f'Processors: {systems["ProcessorSummary_Count"]}x'
        msg += (
            f' {systems["ProcessorSummary_Model"]}'
            if systems['ProcessorSummary_Model']
            else ''
        )
        msg += (
            f' ({systems["ProcessorSummary_LogicalProcessorCount"]} logical)'
            if systems['ProcessorSummary_LogicalProcessorCount']
            else ''
        )
        msg += ', '
        msg += f'BIOS: {systems["BiosVersion"]}, '
        msg += f'Power: {systems["PowerState"]}, ' if systems['PowerState'] else ''
        msg += f'LED: {systems["IndicatorLED"]}, ' if systems['IndicatorLED'] else ''
        msg += f'SKU: {systems["SKU"]}, ' if systems['SKU'] else ''
        msg += f'SerNo: {systems["SerialNumber"]}, ' if systems['SerialNumber'] else ''
        msg = msg[:-2] + lib.base.state2str(systems_state, prefix=' ')
        msg += '\n\n'

    # --inventory: emit the collected components as JSON and exit before
    # building the human-readable message and perfdata
    if args.INVENTORY:
        print(
            json.dumps(
                {'system': inventory}, ensure_ascii=False, indent=4, sort_keys=True
            )
        )
        sys.exit(STATE_OK)

    # build the message
    members = lib.txt.pluralize('member', member_count)
    if state == STATE_CRIT:
        msg = (
            f'Checked system health on {member_count} {members}.'
            f' There are critical errors.\n\n'
        ) + msg
    elif state == STATE_WARN:
        msg = (
            f'Checked system health on {member_count} {members}.'
            f' There are warnings.\n\n'
        ) + msg
    else:
        msg = (
            f'Everything is ok. Checked system health on {member_count} {members}.\n\n'
        ) + msg

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