#!/usr/bin/env python3
# -*- encoding: utf-8; py-indent-offset: 4 -*-
#
# Copyright (C) 2026 comNET GmbH
# <https://www.comnet-solutions.de/>
#
# This is free software;  you can redistribute it and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the Free Software Foundation in version 2.  This file is distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.
#


import argparse
import logging
import os.path
import socket
import sys
from argparse import ArgumentParser
from io import StringIO
from pathlib import Path

from cmk.utils import password_store
from cmk.utils.password_store import replace_passwords
from cmk_addons.plugins.huawei_oceanstor.lib import oceanstor

__VERSION__ = '240.0.12'

perf_data_ids = {
    11: [  # LUN
        19,  # Queue length
        21,  # Block bandwidth
        23,  # Read bandwidth (MB/s)
        24,  # Average Read I/O Size (KB)
        25,  # Read IOPS (IO/s)
        26,  # Write bandwidth (MB/s)
        27,  # Average Write I/O Size (KB)
        28,  # Write IOPS (IO/s)
        370,  # Average I/O response time (ms)
        384,  # Read I/O response time (ms)
        385,  # Write I/O response time (ms)
    ],
    207: [  # Controller
        19,  # Queue length
        21,  # Block bandwidth
        22,  # Total IOPS
        23,  # Read bandwidth
        25,  # Read IOPS
        26,  # Write bandwidth
        28,  # Write IOPS
        69,  # Average cache usage (%)
        93,  # Read cache hit ratio (%)
        95,  # Write cache hit ratio (%)
        110,  # Cache read usage (%)
        120,  # Cache write usage (%)
        303,  # Hit ratio (%)
        370,  # Average I/O response time (us)
        1055,  # Cache page utilization (%)
        1056,  # Cache chunk utilization (%)
    ],
    212: [  # FC Port
        18,  # Usage
        19,  # Queue length
        21,  # Block bandwidth
        23,  # Read bandwidth (MB/s)
        24,  # Average Read I/O Size (KB)
        25,  # Read IOPS (IO/s)
        26,  # Write bandwidth (MB/s)
        27,  # Average Write I/O Size (KB)
        28,  # Write IOPS (IO/s)
    ],
    213: [  # Ethernet Port
        18,  # Usage
        19,  # Queue length
        21,  # Block bandwidth
        23,  # Read bandwidth (MB/s)
        24,  # Average Read I/O Size (KB)
        25,  # Read IOPS (IO/s)
        26,  # Write bandwidth (MB/s)
        27,  # Average Write I/O Size (KB)
        28,  # Write IOPS (IO/s)
    ],
    216: [  # Storagepool
        19,  # Queue length
        21,  # Block bandwidth
        23,  # Read bandwidth (MB/s)
        24,  # Average Read I/O Size (KB)
        25,  # Read IOPS (IO/s)
        26,  # Write bandwidth (MB/s)
        27,  # Average Write I/O Size (KB)
        28,  # Write IOPS (IO/s)
    ],
    252: [  # FCoE Port
        18,  # Usage
        19,  # Queue length
        21,  # Block bandwidth
        23,  # Read bandwidth (MB/s)
        24,  # Average Read I/O Size (KB)
        25,  # Read IOPS (IO/s)
        26,  # Write bandwidth (MB/s)
        27,  # Average Write I/O Size (KB)
        28,  # Write IOPS (IO/s)
    ],
}

# Fields to always print for any items
static_fields = [
    'ID',
    'NAME',
    'LOCATION',
    'MODEL',
    'HEALTHSTATUS',
    'RUNNINGSTATUS',
    'TEMPERATURE',
    'TYPE',
]

# Items to query, with additional (specific) fields
query_items = {
    'alarm_currentalarm': ("alarm/currentalarm", [
        'sequence',
        'description',
        'level',
        'location',
        'startTime',
    ]),
    'bond_port': [
    ],
    'backup_power': [
        'VOLTAGE',
        'REMAINLIFEDAYS',
        'POWERTYPE',
        'RUNMODE',
    ],
    'CONSISTENTGROUP': [
        'ISEMPTY',
        'ISPRIMARY',
        'REPLICATIONMODEL',
    ],
    'controller': [
        'CPUUSAGE',
        'ISMASTER',  # Is master?
        'MEMORYUSAGE',
        'MEMORYSIZE',
        'ROLE',
        'RUNMODE',  # Like interface modules (FC, FCoE/iSCSI or Cluster)
        'SOFTVER',  # Software version
    ],
    'disk': [
        'ABRASIONRATE',
        'BANDWIDTH',
        'DISKTYPE',
        'formatProgress',
        'formatRemainTime',
        'HEALTHMARK',
        'ISCOFFERDISK',
        'MANUFACTURER',
        'LOGICTYPE',
        'REMAINLIFE',
        'SECTORS',
        'SECTORSIZE',
        'SPEEDRPM',
    ],
    'diskpool': [
        'SPARECAPACITY',
        'TOTALCAPACITY',
        'USEDCAPACITY',
        'USEDSPARECAPACITY',
        'NLSASSPARECAPACITY',
        'NLSASTOTALCAPACITY',
        'NLSASUSEDCAPACITY',
        'NLSASUSEDSPARECAPACITY',
        'SASSPARECAPACITY',
        'SASTOTALCAPACITY',
        'SASUSEDCAPACITY',
        'SASUSEDSPARECAPACITY',
        'SSDSPARECAPACITY',
        'SSDTOTALCAPACITY',
        'SSDUSEDCAPACITY',
        'SSDUSEDSPARECAPACITY',
    ],
    'enclosure': [
        'LOGICTYPE',
    ],
    'eth_port': [
        'crcErrors',
        'ERRORPACKETS',
        'frameErrors',
        'frameLengthErrors',
        'INIORTGT',
        'LOGICPORT',
        'LOSTPACKETS',
        'MACADDRESS',
        'OVERFLOWEDPACKETS',
        'OWNINGCONTROLLER',
        'PORTSWITCH',
        'SPEED',
    ],
    'expboard': [
    ],
    'fan': [
        'RUNLEVEL',  # low, normal, high
    ],
    'fc_port': [
        'BADCHARNUMBER',
        'BADCRCNUM',
        'endOfFrameErrors',
        'FCCONFMODE',
        'FCRUNMODE',
        'FLOGINDELAYTIMES',
        'INIORTGT',
        'LINKFAIL',
        'LOSTSIGNALS',
        'LOSTSYNC',
        'PORTSWITCH',  # on or off
        'CONFSPEED',
        'RUNSPEED',
        'MAXSPEED',
        'SFPSTATUS',
        'WWN',
    ],
    'fcoe_port': [
        'ERRORPACKETS',
        'INIORTGT',
        'LOSTPACKETS',
        'OVERFLOWEDPACKETS',
        'PORTSWITCH',  # on or off
        'RUNSPEED',
        'MAXSPEED',
        'SFPSTATUS',
        'WWN',
    ],
    'filesystem': [
        'allocatedPoolQuota',
        'ALLOCCAPACITY',
        'ALLOCTYPE',
        'CAPACITY',
        'CAPACITYTHRESHOLD',
        'READONLY',
        'SECTORSIZE',
        'SNAPSHOTRESERVECAPACITY',
        'SNAPSHOTUSECAPACITY',
        'SUBTYPE',
    ],
    'HyperMetro_ConsistentGroup': [
        'DOMAINNAME',
        'ISEMPTY',
        'RESOURCETYPE',
    ],
    'HyperMetroDomain': [
    ],
    'HyperMetroPair': [
        'DOMAINNAME',
        'HCRESOURCETYPE',
        'LINKSTATUS',
        'LOCALDATASTATE',
        'LOCALHOSTACCESSSTATE',
        'LOCALOBJNAME',
        'REMOTEDATASTATE',
        'REMOTEHOSTACCESSSTATE',
        'REMOTEOBJNAME',
    ],
    'ib_port': [
        'EXCESSIVEBUFFEROVERRUNERRORS',
        'LINKERRORRECOVERYCOUNTER',
        'LOCALLINKINTEGRITYERRORS',
        'PORTRCVCONSTRAINTERRORS',
        'PORTRCVERRORS',
        'PORTRCVREMOTEPHYSICALERRORS',
        'PORTRCVSWITCHRELAYERRORS',
        'PORTXMITCONSTRAINTERRORS',
        'RUNSPEED',
        'MAXSPEED',
        'SYMBOLERRORCOUNTER',
    ],
    'intf_module': [
        'RUNMODE',
    ],
    'iscsi_link': [
        'IP',
        'PORT',
    ],
    'lun': [
        'ALLOCCAPACITY',
        'ALLOCTYPE',
        'CAPACITY',
        'ENABLEISCSITHINLUNTHRESHOLD',
        'EXPOSEDTOINITIATOR',
        'ISCSITHINLUNTHRESHOLD',
        'SECTORSIZE',
        'SUBTYPE',
        'WWN',
    ],
    'power': [
        'INPUTVOLTAGE',
        'OUTPUTVOLTAGE',
        'POWERTYPE',
        'RUNMODE',
    ],
    'QuorumServer': [
        'SERVERIPA',
        'SERVERIPB',
    ],
    'REPLICATIONPAIR': [
        'ISDATASYNC',
        'ISPRIMARY',
        'LOCALRESNAME',
        'PRIRESDATASTATUS',
        'REMOTERESNAME',
        'REPLICATIONMODEL',
        'SECRESDATASTATUS',
    ],
    'sas_port': [
        'DISPARITYERROR',
        'INVALIDDWORD',
        'LOSSDWORD',
        'PHYRESETERRORS',
        'RUNSPEED',
        'MAXSPEED',
    ],
    'sfp': [
        'RXPOWER',
        'TXPOWER',
    ],
    'storagepool': [
        'USERCONSUMEDCAPACITY',
        'USERCONSUMEDCAPACITYTHRESHOLD',
        'USERTOTALCAPACITY',
        'USAGETYPE',
        'LUNCONFIGEDCAPACITY',
        'compressedCapacity',  # OceanStor V5
        'dedupedCapacity',  # OceanStor V5
        'COMPRESSEDCAPACITY',  # OceanStor Dorado V3
        'DEDUPEDCAPACITY',  # OceanStor Dorado V3
    ],
    'system': ("system/", [  # system is only one object (no list), and needs trailing slash
        'CACHEWRITEQUOTA',
        'FREEDISKSCAPACITY',
        'HOTSPAREDISKSCAPACITY',
        'MEMBERDISKSCAPACITY',
        'PRODUCTMODE',
        'PRODUCTVERSION',
        'STORAGEPOOLCAPACITY',
        'STORAGEPOOLFREECAPACITY',
        'STORAGEPOOLHOSTSPARECAPACITY',
        'STORAGEPOOLRAWCAPACITY',
        'STORAGEPOOLUSEDCAPACITY',
        'THICKLUNSALLOCATECAPACITY',
        'THICKLUNSUSEDCAPACITY',
        'THINLUNSALLOCATECAPACITY',
        'THINLUNSMAXCAPACITY',
        'THINLUNSUSEDCAPACITY',
        'TOTALCAPACITY',
        'UNAVAILABLEDISKSCAPACITY',
        'USEDCAPACITY',
    ]),
    'host': [
        'DESCRIPTION',
        'OPERATIONSYSTEM',
        'NETWORKNAME',
        'IP',
    ],
}


def tcp_port(string):
    try:
        port = int(string)
        if port < 1 or port > 65534:
            raise ValueError()
        return port
    except ValueError:
        msg = u'Port {} is not a valid integer in range 1-65534'.format(string)
        raise argparse.ArgumentTypeError(msg)


parser = ArgumentParser(description='Check_MK Huawei OceanStor special agent')
parser.add_argument('-P', '--port',
                    dest='port', default=8088, type=tcp_port,
                    help='TCP port to connect to')
parser.add_argument('-l', '--ldap',
                    action='store_true', dest='ldap', default=False,
                    help='Login on LDAP scope')
parser.add_argument('-i', '--insecure',
                    action='store_true', dest='insecure', default=False,
                    help='Do not validate TLS certificate')
parser.add_argument('-t', '--timeout',
                    dest='timeout', default=10, type=int,
                    help='Connection timeout')
parser.add_argument('-s', '--session-file',
                    dest='session_file_path',
                    help='Overwrite path of the session file (for storing session cookie etc.)')
parser.add_argument('-c', '--close-session',
                    action='store_true', dest='close_session', default=False,
                    help='Close session (logout) when done.\n' +
                         'This also means the session file will not be used.' +
                         'DO NOT USE THIS for regular monitoring unless you want' +
                         'your event logs filled with logins and logouts.')
parser.add_argument('-d', '--debug',
                    action='store_true', dest='debug', default=False,
                    help='Debug mode: raise Python exceptions')
parser.add_argument('-v', '--verbose',
                    action='store_true', dest='verbose', default=False,
                    help='Be more verbose')
parser.add_argument('--endpoints', dest='endpoints', default=False,
                    help='Comma-separated list of endpoint items to fetch.')
parser.add_argument('--perfdata', dest='perfdata', default=False,
                    help='''Comma-separated list of perfdata items to gather.
                          Possible values:
                            11  (LUN)
                            207 (Controller)
                            212 (FC port)
                            213 (Ethernet port)
                            216 (Storage pool)
                            252 (FCoE port)
                        ''')
parser.add_argument('-C', '--cpu-data',
                    action='store_true', dest='cpu_data', default=False,
                    help='Try to retrieve CPU data. This requires Administrator privileges.')

parser.add_argument('-H', '--host', dest='host',
                    help='OceanStor node to connect to')
parser.add_argument('-u', '--username', dest='username',
                    help='Login username')
parser.add_argument(
    "--password-reference",
    help="Password store reference of the password for the login.", )
parser.add_argument('-p', '--password', dest='password',
                    help='Login password')

replace_passwords()
args = parser.parse_args()

socket.setdefaulttimeout(args.timeout)

selected_endpoints = [] if not args.endpoints else args.endpoints.split(',')
filtered_query_items = {}
for endpoint_key in selected_endpoints:
    url = endpoint_key
    items = query_items[endpoint_key]
    if isinstance(items, tuple):
        url, items = items
    filtered_query_items[url] = items
query_items = filtered_query_items


def convert_perfdata(value: str) -> int:
    if value.startswith("p"):
        return int(value[1:])
    return int(value)


perfdata_types = [] if not args.perfdata else list(map(convert_perfdata, args.perfdata.split(',')))

if args.debug:
    level = logging.DEBUG
elif args.verbose:
    level = logging.INFO
else:
    level = logging.WARNING
logging.basicConfig(level=level)

# If CPU data should be retrieved, append it to query_items
if args.cpu_data:
    query_items['cpu'] = [
        'CORETEMP',
        'VOLTS',
    ]


def get_perfdata_for_item(item_type, item_id):
    '''
    Obtain the performance statistics for the given item (type and id)
    as a string of semicolon-separated ID:VALUE pairs, e. g.:

        <ID1>:<VALUE1>;<ID2>:<VALUE2>...

    Returns an empty string if there are no viable performance values
    for the given item type, or an exception occurs when obtaining them.
    '''

    ids = perf_data_ids.get(item_type, [])
    if not ids:
        return ''

    if args.debug:
        sys.stderr.write('Item Type: {}\n'.format(item_type))
        sys.stderr.write('Item ID: {}\n'.format(item_id))
        sys.stderr.write('Perf IDs: {}\n'.format(ids))

    url = 'performace_statistic/cur_statistic_data?CMO_STATISTIC_UUID={}:{}&CMO_STATISTIC_DATA_ID_LIST={}'.format(
        item_type,
        item_id,
        ','.join(map(str, ids)),
    )
    try:
        perfdata = dm.get(url)[0]
        perfdata = zip(
            perfdata['CMO_STATISTIC_DATA_ID_LIST'].split(','),
            perfdata['CMO_STATISTIC_DATA_LIST'].split(',')
        )
        return ';'.join(['{}:{}'.format(k, v) for k, v in perfdata])
    except Exception as e:
        if args.debug:
            raise
        return ''


def get_associated_hosts_for_item(item_type, item_id):
    '''
    Obtain the hosts associated to an item (LUN).
    '''
    url = 'host/associate?ASSOCIATEOBJTYPE={}&ASSOCIATEOBJID={}'.format(
        item_type,
        item_id,
    )
    try:
        hostlist = []
        hostdata = dm.get(url)
        for host in hostdata:
            hostlist.append(host['NAME'])
        return ';'.join(hostlist)
    except Exception as e:
        if args.debug:
            raise
        return ''


def get_result(getter, key, step=1000):
    '''
    Get items for the given key in ranges of the given step size (defaults
    to 1000 items per request), using the given getter function.

    This function returns a tuple containing the keys and result items, like so:

        return keys, result

    where `keys` is a list containing all available / present fields, and `result`
    is a list containing all existing items of that API key / element, each being
    a dict with the aforementioned keys.

    Keys ending in '/' should return a single item. For such reqeusts, this function
    returns the keys of this item and the item wrapped as a 1-item list.

    Other keys are requested in ranges, extending the result list and finally
    returning all items in a single result list.

    If there are no result items, this function returns two empty lists, meaning
    no keys, no results.
    '''
    if key.endswith('/'):  # Will result in a single dict, instead of list of items
        result = getter(key)
        return result.keys(), [result]

    if key.endswith('_port'):  # Ports don't know the range parameter
        result = getter(key)
    else:
        result = get_result_with_range(getter, key, step)

    if not result:
        return [], []
    return result[0].keys(), result


def get_result_with_range(getter, key, step):
    result = []
    page = 0
    while True:
        url = key + '?range=[{}-{}]'.format(page * step, (page + 1) * step)
        range_result = getter(url) or []
        page += 1
        result += range_result
        if len(range_result) < step:
            break
    return result


def process_section(section):
    key, fields = section
    out = StringIO()
    out.write(u'<<<huawei_oceanstor_{}:sep(124)>>>\n'.format(key.lower().split('/')[0]))
    try:
        present_fields, items = get_result(dm.get, key)
        if items:
            fields = [f for f in static_fields + fields if f in present_fields]
            fields.append('ASSOCIATEDHOSTS')
            if args.perfdata:
                fields.append('PERF')

            out.write(u'|'.join(fields) + '\n')

            def process_item(item):
                if args.perfdata and item.get('TYPE') in perfdata_types:
                    item['PERF'] = get_perfdata_for_item(item['TYPE'], item['ID'])

                if item.get('TYPE') == 11:
                    item['ASSOCIATEDHOSTS'] = get_associated_hosts_for_item(item['TYPE'], item['ID'])

                # Can directly write to out since order does not matter for items within the section
                out.write(u'|'.join([str(item.get(f, '')).replace('\n', '\\n') for f in fields]) + '\n')

            for item in items:
                process_item(item)
    except oceanstor.APIError as e:
        if args.debug:
            raise
        out.write(u'ERROR|{}\n'.format(e.description))
    except Exception as e:
        if args.debug:
            raise
        try:
            if e.response.status_code == 404:
                # Assume it's a requests.exceptions.HTTPError with status code 404 (Not found)
                # If it is, just silently continue to the next query item.
                return
        except:  # Otherwise, just move on to writing the error to stdout.
            pass
        out.write(u'ERROR|{}: {!s}\n'.format(type(e).__name__, e))
    return out.getvalue()


def _make_secret(args: argparse.Namespace) -> str:
    if (ref := args.password_reference) is None:
        return args.password

    pw_id, pw_file = ref.split(":", 1)
    return password_store.lookup(Path(pw_file), pw_id)



try:
    sys.stdout.write(u'<<<check_mk>>>\n')
    sys.stdout.write(u'Version: {}\n'.format(__VERSION__))
    sys.stdout.write(u'Hostname: {}\n'.format(args.host))

    dm = oceanstor.DeviceManager(args.host, port=args.port,
                                 timeout=args.timeout, insecure=args.insecure)

    if not args.close_session:
        session_file_path = os.path.expanduser(
            args.session_file_path or os.path.join('~', 'tmp', 'huawei_oceanstor_{}_{}'.format(
                args.host,
                args.port,
            ))
        )
        if os.path.isfile(session_file_path):
            dm.load_session_from_file(session_file_path)

    secret = _make_secret(args)

    dm.authenticate(args.username, secret, scope=1 if args.ldap else 0)

    if not args.close_session:
        dm.save_session_to_file(session_file_path)

    sections = []
    for item in query_items.items():
        sections.append(process_section(item))

    # Even though we process sections concurrently, we write them to stdout sequentially.
    # This is to make sure they don't mix up with eachother, which would not allow
    # processing the agent output correctly.
    for section in sections:
        if section:  # Sections that raised HTTP 404 are None here which cannot be written to stdout
            sys.stdout.write(section)

    if args.close_session:
        dm.close()
except Exception as e:
    if args.debug:
        raise
    sys.stderr.write(u'Cannot query OceanStor controller node. {}\n'.format(e))
    sys.exit(1)
