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

import lib.args  # pylint: disable=C0413
import lib.base  # pylint: disable=C0413
import lib.human  # pylint: disable=C0413
import lib.lftest  # pylint: disable=C0413
import lib.net  # pylint: disable=C0413
import lib.txt  # pylint: disable=C0413
import lib.url  # pylint: disable=C0413
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN  # pylint: disable=C0413


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

DESCRIPTION = """This check shows you an abundance of metrics that cover the health of your
                 HAProxy server, current request rates, response times, and more."""

DEFAULT_CRIT = 95  # %
DEFAULT_INSECURE = False
DEFAULT_LENGTHY = False
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 3
DEFAULT_URL = 'unix:///run/haproxy.sock'
DEFAULT_USERNAME = 'haproxy-stats'
DEFAULT_WARN = 80  # %

GOOD_STATUSES = {'OPEN', 'UP', 'no check'}


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

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

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

    parser.add_argument(
        '-c', '--critical',
        help='Set the CRIT threshold as a percentage. '
             'Default: >= %(default)s',
        dest='CRIT',
        type=int,
        default=DEFAULT_CRIT,
    )

    parser.add_argument(
        '--insecure',
        help='This option explicitly allows to perform "insecure" SSL connections. '
             'Default: %(default)s',
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

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

    parser.add_argument(
        '--no-proxy',
        help='Do not use a proxy. '
             'Default: %(default)s',
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '-p', '--password',
        help='HAProxy Stats Auth password (not needed for socket access).',
        dest='PASSWORD',
    )

    parser.add_argument(
        '--test',
        help='For unit tests. Needs "path-to-stdout-file,path-to-stderr-file,expected-retc".',
        dest='TEST',
        type=lib.args.csv,
    )

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

    parser.add_argument(
        '-u', '--url',
        help='HAProxy Stats URI. Can be either `unix:///path/to/haproxy.sock` or an URL like '
             '`https://webserver:8443/server-status`. '
             'Default: %(default)s',
        dest='URL',
        default=DEFAULT_URL,
    )

    parser.add_argument(
        '--username',
        help='HAProxy Stats Auth username (not needed for socket access). '
             'Default: %(default)s',
        dest='USERNAME',
        default=DEFAULT_USERNAME,
    )

    parser.add_argument(
        '-w', '--warning',
        help='Set the WARN threshold as a percentage. '
             'Default: >= %(default)s',
        dest='WARN',
        type=int,
        default=DEFAULT_WARN,
    )

    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)

    # fetch data
    if args.TEST is None:
        url = args.URL
        if not (url.startswith('http') or url.startswith('unix://')):
            lib.base.oao(
                '`--url` parameter must start with "http://", "https://" or "unix://"',
                STATE_UNKNOWN
            )

        if url.startswith('http'):
            url += ';csv'
            headers = {}
            if args.USERNAME and args.PASSWORD:
                auth = f'{args.USERNAME}:{args.PASSWORD}'
                encoded_auth = lib.txt.to_text(base64.b64encode(lib.txt.to_bytes(auth)))
                headers = {'Authorization': f'Basic {encoded_auth}'}
            result = lib.base.coe(lib.url.fetch(
                url,
                header=headers,
                insecure=args.INSECURE,
                no_proxy=args.NO_PROXY,
                timeout=args.TIMEOUT,
            ))
        elif url.startswith('unix://'):
            result = lib.base.coe(lib.net.fetch_socket(
                url.replace('unix://', ''),
                lib.txt.to_bytes('show stat\n')
            ))
    else:
        # do not call the command, put in test data
        stdout, _, _ = lib.lftest.test(args.TEST)
        result = stdout

    result = result.strip().split('\n')  # pylint: disable=E0601

    # init some vars
    msg_parts = []
    state = STATE_OK
    perfdata_parts = []
    table_values = []

    # analyze data
    for line in result[1:]:
        values = [x.strip() for x in line.strip().split(',')]

        # ugly, but necessary - the csv is huge
        try:
            ha_pxname = values[0]
            ha_svname = values[1]
            ha_qcur = values[2]
            ha_qmax = values[3] # pylint: disable=W0612
            ha_scur = values[4]
            ha_smax = values[5] # pylint: disable=W0612
            ha_slim = values[6]
            ha_stot = values[7]
            ha_bin = values[8]
            ha_bout = values[9]
            ha_dreq = values[10]
            ha_dresp = values[11]
            ha_ereq = values[12]
            ha_econ = values[13]
            ha_eresp = values[14]
            ha_wretr = values[15]
            ha_wredis = values[16]
            ha_status = values[17]
            ha_weight = values[18]
            ha_act = values[19]
            ha_bck = values[20]
            ha_chkfail = values[21]
            ha_chkdown = values[22]
            ha_lastchg = values[23]
            ha_downtime = values[24]
            ha_qlimit = values[25]
            ha_pid = values[26] # pylint: disable=W0612
            ha_iid = values[27] # pylint: disable=W0612
            ha_sid = values[28] # pylint: disable=W0612
            ha_throttle = values[29] # pylint: disable=W0612
            ha_lbtot = values[30]
            ha_tracked = values[31]
            ha_type = values[32] # pylint: disable=W0612
            ha_rate = values[33]
            ha_rate_lim = values[34]
            ha_rate_max = values[35] # pylint: disable=W0612
            ha_check_status = values[36] # pylint: disable=W0612
            ha_check_code = values[37] # pylint: disable=W0612
            ha_check_duration = values[38] # pylint: disable=W0612
            ha_hrsp_1xx = values[39]
            ha_hrsp_2xx = values[40]
            ha_hrsp_3xx = values[41]
            ha_hrsp_4xx = values[42]
            ha_hrsp_5xx = values[43]
            ha_hrsp_other = values[44]
            ha_hanafail = values[45]
            ha_req_rate = values[46]
            ha_req_rate_max = values[47] # pylint: disable=W0612
            ha_req_tot = values[48]
            ha_cli_abrt = values[49]
            ha_srv_abrt = values[50]
            ha_comp_in = values[51]
            ha_comp_out = values[52]
            ha_comp_byp = values[53]
            ha_comp_rsp = values[54]
            ha_lastsess = values[55]
            ha_last_chk = values[56]
            ha_last_agt = values[57] # pylint: disable=W0612
            ha_qtime = values[58]
            ha_ctime = values[59]
            ha_rtime = values[60]
            ha_ttime = values[61]
        except (ValueError, IndexError) as e:
            lib.base.cu('Malformed HAProxy status info.')

        # Status check
        status_state = STATE_OK
        if ha_status not in GOOD_STATUSES:
            status_state = STATE_WARN
            msg_parts.append(f'{ha_pxname} {ha_svname}: {ha_status}, ')
            state = lib.base.get_worst(status_state, state)

        # Queued connections
        if ha_qcur and ha_qlimit and float(ha_qlimit) != 0:
            q_percentage = round(float(ha_qcur) / float(ha_qlimit) * 100, 1)
            q_state = lib.base.get_state(q_percentage, args.WARN, args.CRIT)
            if q_state != STATE_OK:
                msg_parts.append(
                    f'{ha_pxname} {ha_svname}: {ha_qcur} queued ({q_percentage}%)'
                    f'{lib.base.state2str(q_state, prefix=" ")}, '
                )
                state = lib.base.get_worst(q_state, state)

        # Session usage
        if ha_scur and ha_slim and float(ha_slim) != 0:
            s_percentage = round(float(ha_scur) / float(ha_slim) * 100, 1)
            s_state = lib.base.get_state(s_percentage, args.WARN, args.CRIT)
            if s_state != STATE_OK:
                msg_parts.append(
                    f'{ha_pxname} {ha_svname}: {ha_scur} sessions ({s_percentage}%)'
                    f'{lib.base.state2str(s_state, prefix=" ")}, '
                )
                state = lib.base.get_worst(s_state, state)

        # Rate check
        if ha_rate and ha_rate_lim and float(ha_rate_lim) != 0:
            r_percentage = round(float(ha_rate) / float(ha_rate_lim) * 100, 1)
            r_state = lib.base.get_state(r_percentage, args.WARN, args.CRIT)
            if r_state != STATE_OK:
                msg_parts.append(
                    f'{ha_pxname} {ha_svname}: {ha_rate} sessions/sec ({r_percentage}%)'
                    f'{lib.base.state2str(r_state, prefix=" ")}, '
                )
                state = lib.base.get_worst(r_state, state)

        # Build table entry
        table_values.append({
            'pxname': ha_pxname,
            'svname': ha_svname,
            'qcur': f'{ha_qcur}/{ha_qlimit}' if ha_qlimit else ha_qcur,
            'scur': f'{ha_scur}/{ha_slim}' if ha_slim else ha_scur,
            'bin': lib.human.bytes2human(int(ha_bin)) if ha_bin else '',
            'bout': lib.human.bytes2human(int(ha_bout)) if ha_bout else '',
            'status': f'{ha_status}{lib.base.state2str(status_state, prefix=" ")}',
            'lbtot': ha_lbtot,
            'rate': f'{ha_rate}/{ha_rate_lim}' if ha_rate_lim else ha_rate,
            'hrsp_2xx': ha_hrsp_2xx,
            'hrsp_4xx': ha_hrsp_4xx,
            'hrsp_5xx': ha_hrsp_5xx,
            'req_rate': ha_req_rate,
            'lastsess': (
                lib.human.seconds2human(int(ha_lastsess))
                if ha_lastsess not in ('', '-1') else ''
            ),
            'ttime': ha_ttime,
        })

        key = f'{ha_pxname}_{ha_svname}'

        perf_items = [
            ('act', ha_act, None),
            ('bck', ha_bck, None),
            ('bin', ha_bin, 'B'),
            ('bout', ha_bout, 'B'),
            ('chkdown', ha_chkdown, 'c'),
            ('chkfail', ha_chkfail, 'c'),
            ('cli_abrt', ha_cli_abrt, 'c'),
            ('comp_byp', ha_comp_byp, 'B'),
            ('comp_in', ha_comp_in, 'B'),
            ('comp_out', ha_comp_out, 'B'),
            ('comp_rsp', ha_comp_rsp, 'c'),
            ('ctime', ha_ctime, 'ms'),
            ('downtime', ha_downtime, 's'),
            ('dreq', ha_dreq, 'c'),
            ('dresp', ha_dresp, 'c'),
            ('econ', ha_econ, 'c'),
            ('ereq', ha_ereq, 'c'),
            ('eresp', ha_eresp, 'c'),
            ('hanafail', ha_hanafail, None),
            ('hrsp_1xx', ha_hrsp_1xx, 'c'),
            ('hrsp_2xx', ha_hrsp_2xx, 'c'),
            ('hrsp_3xx', ha_hrsp_3xx, 'c'),
            ('hrsp_4xx', ha_hrsp_4xx, 'c'),
            ('hrsp_5xx', ha_hrsp_5xx, 'c'),
            ('hrsp_other', ha_hrsp_other, 'c'),
            ('last_chk', ha_last_chk, 's'),
            ('lastchg', ha_lastchg, 's'),
            ('lastsess', ha_lastsess, 's'),
            ('lbtot', ha_lbtot, 'c'),
            ('qcur', ha_qcur, None),
            ('qlimit', ha_qlimit, None),
            ('qtime', ha_qtime, 'ms'),
            ('rate', ha_rate, None),
            ('rate_lim', ha_rate_lim, None),
            ('req_rate', ha_req_rate, None),
            ('req_tot', ha_req_tot, 'c'),
            ('rtime', ha_rtime, 'ms'),
            ('scur', ha_scur, None),
            ('slim', ha_slim, None),
            ('srv_abrt', ha_srv_abrt, 'c'),
            ('stot', ha_stot, 'c'),
            ('tracked', ha_tracked, None),
            ('ttime', ha_ttime, 'ms'),
            ('weight', ha_weight, None),
            ('wredis', ha_wredis, 'c'),
            ('wretr', ha_wretr, 'c'),
        ]

        for fieldname, value, unit in perf_items:
            if value:
                perfdata_parts.append(lib.base.get_perfdata(
                    f'{key}_{fieldname}', value, uom=unit, warn=None, crit=None, _min=0, _max=None
                ))

    # build the message
    msg = ''.join(msg_parts).rstrip(', ') if msg_parts else 'Everything is ok.'
    msg += '\n\n'

    if not args.LENGTHY:
        msg += lib.base.get_table(
            table_values,
            [
                'pxname',
                'svname',
                'scur',
                'bin',
                'bout',
                'hrsp_5xx',
                'req_rate',
                'status',
            ],
            header=[
                'Proxy name',
                'Server name',
                'Sessions',
                'RqBytes',
                'RspBytes',
                'Rsp5xx',
                'Rq/s',
                'Status',
            ],
        )
    else:
        msg += lib.base.get_table(
            table_values,
            [
                'pxname',
                'svname',
                'qcur',
                'scur',
                'bin',
                'bout',
                'lbtot',
                'rate',
                'hrsp_2xx',
                'hrsp_4xx',
                'hrsp_5xx',
                'req_rate',
                'lastsess',
                'ttime',
                'status',
            ],
            header=[
                'Proxy name',
                'Server name',
                'Queued',
                'Sessions',
                'RqBytes',
                'RspBytes',
                'RqLB',
                'Rate',
                'Rsp2xx',
                'Rsp4xx',
                'Rsp5xx',
                'Rq/s',
                'LastReq',
                'RqRspTime',
                'Status',
            ],
        )

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


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