#!/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.args  # pylint: disable=C0413
import lib.base  # pylint: disable=C0413
import lib.cache  # pylint: disable=C0413
import lib.human  # pylint: disable=C0413
import lib.lftest  # pylint: disable=C0413
import lib.url  # pylint: disable=C0413
from lib.globals import (STATE_CRIT, STATE_OK,  # pylint: disable=C0413
                          STATE_UNKNOWN, STATE_WARN)

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

DESCRIPTION = 'Checks how well an Apache httpd server is performing.'

DEFAULT_CRIT = 95       # %
DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_TIMEOUT = 8
DEFAULT_URL = 'http://localhost/server-status'
DEFAULT_WARN = 80       # %


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(
        '-c', '--critical',
        help='Set the CRIT threshold for the number of workers processing '
             'requests in percent. 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(
        '--no-proxy',
        help='Do not use a proxy. '
             'Default: %(default)s',
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    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='Apache Server Status URL. Default: %(default)s',
        dest='URL',
        default=DEFAULT_URL,
    )

    parser.add_argument(
        '-w', '--warning',
        help='Set the WARN threshold for the number of workers processing '
             'requests in percent. 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 URL[0:4] != 'http':
            lib.base.oao(
                '--url parameter has to start with "http://" or https://".',
                STATE_UNKNOWN,
            )
        URL = URL + '?auto'

        # fetch the URL
        result = lib.base.coe(lib.url.fetch(
            URL,
            insecure=args.INSECURE,
            no_proxy=args.NO_PROXY,
            timeout=args.TIMEOUT,
        ))
        if 'Scoreboard: ' not in result:
            lib.base.cu('Malformed Apache server info.')
    else:
        # do not call the command, put in test data
        stdout, stderr, retc = lib.lftest.test(args.TEST)
        result = stdout
    result = result.strip().split('\n')

    # init some vars
    state = STATE_OK
    perfdata = ''
    apache = {}
    workers = {}

    apache['ServerName'] = 'N/A'

    # analyze data
    # Apache mod_status Source:
    # https://github.com/apache/httpd/blob/trunk/modules/generators/mod_status.c
    # We just want "raw" values that provide a real-time / just-in-time measurement. Values
    # which are calculated by Apache, for example over its service uptime and therefore
    # returning an average over time will be ignored in perfdata.
    # See https://github.com/Linuxfabrik/monitoring-plugins/issues//310.
    # Some continous counters have to be re-calculated (Value_Now - Value_Previous = CurrentValue)
    for item in result:
        if ': ' not in item:
            if item != 'TLSSessionCacheStatus':
                apache['ServerName'] = item
            continue

        try:
            key, value = item.split(': ')
        except:
            continue

        if key == 'ServerUptimeSeconds':
            apache['ServerUptimeSeconds'] = int(value) # continous counter
            continue
        if key == 'Total Accesses':
            apache['Total Accesses'] = int(value)
            continue
        if key == 'Total kBytes':
            apache['Total Bytes'] = int(value) * 1024
            continue
        if key == 'Total Duration':
            apache['Total Duration'] = float(value) / 1000    # convert msec to sec
            continue

        apache[key] = value


    # === The Bascis ===============================================================================
    # first analyze all the items we get even if "Extended Status" is "Off"

    # parse the "scoreboard" and count the elements
    apache['TotalWorkers'] = len(apache['Scoreboard'])
    if apache['TotalWorkers'] == 0:
        lib.base.cu('Malformed Apache server info.')

                                                                # from mod_status.c:
    workers['closing'] = apache['Scoreboard'].count('C')        # SERVER_CLOSING
    workers['dns'] = apache['Scoreboard'].count('D')            # SERVER_BUSY_DNS
    workers['finishing'] = apache['Scoreboard'].count('G')      # SERVER_GRACEFUL
    workers['free'] = apache['Scoreboard'].count('.')           # SERVER_DEAD
    workers['idle'] = apache['Scoreboard'].count('I')           # SERVER_IDLE_KILL
    workers['keepalive'] = apache['Scoreboard'].count('K')      # SERVER_BUSY_KEEPALIVE
    workers['logging'] = apache['Scoreboard'].count('L')        # SERVER_BUSY_LOG
    workers['reading'] = apache['Scoreboard'].count('R')        # SERVER_BUSY_READ
    workers['replying'] = apache['Scoreboard'].count('W')       # SERVER_BUSY_WRITE
    workers['starting'] = apache['Scoreboard'].count('S')       # SERVER_STARTING
    workers['waiting'] = apache['Scoreboard'].count('_')        # SERVER_READY

    # currently the only value we warn about
    apache['WorkerUsagePercentage'] = round(int(apache['BusyWorkers']) / int(apache['TotalWorkers']) * 100.0, 1) # pylint: disable=C0301
    state = lib.base.get_state(apache['WorkerUsagePercentage'], args.WARN, args.CRIT)

    perfdata += lib.base.get_perfdata('WorkerUsagePercentage', apache['WorkerUsagePercentage'], '%', args.WARN, args.CRIT, 0, 100) # pylint: disable=C0301

    perfdata += lib.base.get_perfdata('BusyWorkers', apache['BusyWorkers'], None, None, None, 0, None) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('IdleWorkers', apache['IdleWorkers'], None, None, None, 0, None) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('TotalWorkers', apache['TotalWorkers'], None, None, None, 0, None) # pylint: disable=C0301

    perfdata += lib.base.get_perfdata('workers_closing', workers['closing'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_dns', workers['dns'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_finishing', workers['finishing'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_free', workers['free'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_idle', workers['idle'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_keepalive', workers['keepalive'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_logging', workers['logging'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_reading', workers['reading'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_replying', workers['replying'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_starting', workers['starting'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('workers_waiting', workers['waiting'], None, None, None, 0, apache['TotalWorkers']) # pylint: disable=C0301

    # If ExtendedStatus is off:
    if 'Uptime' not in apache:
        # "ExtendedStatus off" just reports BusyWorkers, IdleWorkers and Scoreboard
        msg = '{}/{} workers busy ({}%{}; {} "G"), {} idle, {} free'.format(
            apache['BusyWorkers'],
            apache['TotalWorkers'],
            apache['WorkerUsagePercentage'],
            lib.base.state2str(state, prefix=' '),
            workers['finishing'],
            apache['IdleWorkers'],
            workers['free'],
        )

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


    # === Second Stage==============================================================================
    # ExtendedStatus is on, but not all values are always present due to different versions of
    # mod_status

    # get previous values
    prev_uptime = lib.cache.get(
        'apache-httpd-status-{}-uptime'.format(args.URL),
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    prev_count = lib.cache.get(
        'apache-httpd-status-{}-total-accesses'.format(args.URL),
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    prev_bcount = lib.cache.get(
        'apache-httpd-status-{}-total-bytes'.format(args.URL),
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )

    # save current values
    lib.cache.set(
        'apache-httpd-status-{}-uptime'.format(args.URL),
        apache['Uptime'],
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    lib.cache.set(
        'apache-httpd-status-{}-total-accesses'.format(args.URL),
        apache['Total Accesses'],
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    lib.cache.set(
        'apache-httpd-status-{}-total-bytes'.format(args.URL),
        apache['Total Bytes'],
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    if prev_uptime is False or prev_count is False or prev_bcount is False:
        lib.base.oao('Waiting for more data (1).', STATE_OK)

    # calculate "raw" values for the current interval from total values (continous counters)
    uptime_diff = int(apache['Uptime']) - int(prev_uptime)
    apache['Total Accesses'] = apache['Total Accesses'] - int(prev_count)
    apache['Total Bytes'] = apache['Total Bytes'] - int(prev_bcount)
    if uptime_diff <= 0 or apache['Total Accesses'] <= 0 or apache['Total Bytes'] < 0:
        lib.base.oao('Waiting for more data (2).', STATE_OK)

    perfdata += lib.base.get_perfdata('Accesses', apache['Total Accesses'], None, None, None, 0, None) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('Bytes', apache['Total Bytes'], 'B', None, None, 0, None) # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('CPULoad', apache['CPULoad'], None, None, None, 0, None) if 'CPULoad' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('Uptime', apache['Uptime'], 's', None, None, 0, None) # pylint: disable=C0301

    if 'Total Duration' not in apache:
        msg = '{}/{} workers busy ({}%{}; {} "G"), {} idle, {} free; {} accesses, {} traffic; Up {}'.format(
            apache['BusyWorkers'],
            apache['TotalWorkers'],
            apache['WorkerUsagePercentage'],
            lib.base.state2str(state, prefix=' '),
            workers['finishing'],
            apache['IdleWorkers'],
            workers['free'],
            lib.human.number2human(apache['Total Accesses']),
            lib.human.bytes2human(apache['Total Bytes']),
            lib.human.seconds2human(apache['Uptime']),
        )

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


    # === Third Stage===============================================================================
    # Some more variables in newer versions of mod_status

    prev_duration_global = lib.cache.get(
        'apache-httpd-status-{}-total-duration'.format(args.URL),
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    lib.cache.set(
        'apache-httpd-status-{}-total-duration'.format(args.URL),
        apache['Total Duration'],
        filename='linuxfabrik-monitoring-plugins-apache-httpd-status.db',
    )
    if prev_duration_global is False:
        lib.base.oao('Waiting for more data (3).', STATE_OK)

    # calculate "raw" values for the current interval from total values (continous counters)
    duration_diff = apache['Total Duration'] - float(prev_duration_global)
    if duration_diff <= 0 or apache['Total Accesses'] <= 0 or apache['Total Bytes'] < 0:
        lib.base.oao('Waiting for more data (4).', STATE_OK)

    perfdata += lib.base.get_perfdata('ConnsAsyncClosing', apache['ConnsAsyncClosing'], None, None, None, 0, None) if 'ConnsAsyncClosing' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('ConnsAsyncKeepAlive', apache['ConnsAsyncKeepAlive'], None, None, None, 0, None) if 'ConnsAsyncKeepAlive' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('ConnsAsyncWriting', apache['ConnsAsyncWriting'], None, None, None, 0, None) if 'ConnsAsyncWriting' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('ConnsTotal', apache['ConnsTotal'], None, None, None, 0, None) if 'ConnsTotal' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('Load1', apache['Load1'], None, None, None, 0, None) if 'Load1' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('Load5', apache['Load5'], None, None, None, 0, None) if 'Load5' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('Load15', apache['Load15'], None, None, None, 0, None) if 'Load15' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('ParentServerConfigGeneration', apache['ParentServerConfigGeneration'], None, None, None, 0, None) if 'ParentServerConfigGeneration' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('ParentServerMPMGeneration', apache['ParentServerMPMGeneration'], None, None, None, 0, None) if 'ParentServerMPMGeneration' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('Processes', apache['Processes'], None, None, None, 0, None) if 'Processes' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('Stopping', apache['Stopping'], None, None, None, 0, None) if 'Stopping' in apache else '' # pylint: disable=C0301
    perfdata += lib.base.get_perfdata('Total Duration', duration_diff, 's', None, None, 0, None) if 'Total Duration' in apache else '' # pylint: disable=C0301

    msg = '{}: {}/{} workers busy ({}%{}; {} "G"), {} idle, {} free; {} accesses, {} traffic; Up {}'.format(
        apache['ServerName'],
        apache['BusyWorkers'],
        apache['TotalWorkers'],
        apache['WorkerUsagePercentage'],
        lib.base.state2str(state, prefix=' '),
        workers['finishing'],
        apache['IdleWorkers'],
        workers['free'],
        lib.human.number2human(apache['Total Accesses']),
        lib.human.bytes2human(apache['Total Bytes']),
        lib.human.seconds2human(apache['Uptime']),
    )

    table_values = []
    table_values.append({
        'key': 'Current Time',
        'value': apache['CurrentTime'],
    })
    table_values.append({
        'key': 'Restart Time',
        'value': apache['RestartTime'],
    })
    table_values.append({
        'key': 'Check Interval',
        'value': lib.human.seconds2human(uptime_diff),
    })
    table_values.append({
        'key': 'Uptime',
        'value': lib.human.seconds2human(apache['Uptime']),
    })
    if 'ConnsTotal' in apache:
        table_values.append({
            'key': 'Connections',
            'value': apache['ConnsTotal'],
        })
    if 'ConnsAsyncWriting' in apache:
        table_values.append({
            'key': '  Async Writing',
            'value': apache['ConnsAsyncWriting'],
        })
    if 'ConnsAsyncKeepAlive' in apache:
        table_values.append({
            'key': '  Async KeepAlive',
            'value': apache['ConnsAsyncKeepAlive'],
        })
    if 'ConnsAsyncClosing' in apache:
        table_values.append({
            'key': '  Async Closing',
            'value': apache['ConnsAsyncClosing'],
        })
    table_values.append({
        'key': 'Requests',
        'value': lib.human.number2human(apache['Total Accesses']),
    })
    table_values.append({
        'key': 'Bytes',
        'value': lib.human.bytes2human(apache['Total Bytes']),
    })
    table_values.append({
        'key': 'Request Duration',
        'value': lib.human.seconds2human(duration_diff),
    })
    if 'Load1' in apache:
        table_values.append({
            'key': 'Load1',
            'value': apache['Load1'],
        })
    if 'Load15' in apache:
        table_values.append({
            'key': 'Load5',
            'value': apache['Load5'],
        })
    if 'Load15' in apache:
        table_values.append({
            'key': 'Load15',
            'value': apache['Load15'],
        })
    if 'Processes' in apache:
        table_values.append({
            'key': 'Processes',
            'value': apache['Processes'],
        })
    if 'Stopping' in apache:
        table_values.append({
            'key': '  Stopping',
            'value': apache['Stopping'],
        })
    table_values.append({
        'key': 'Workers Total',
        'value': apache['TotalWorkers'],
    })
    table_values.append({
        'key': '  Busy',
        'value': apache['BusyWorkers'],
    })
    table_values.append({
        'key': '  Idle',
        'value': apache['IdleWorkers'],
    })
    table_values.append({
        'key': '  Usage (%)',
        'value': str(apache['WorkerUsagePercentage']) + lib.base.state2str(state, prefix=' '),
    })
    if 'ParentServerConfigGeneration' in apache:
        table_values.append({
            'key': 'Parent Server ConfigGeneration',
            'value': apache['ParentServerConfigGeneration'],
        })
    if 'ParentServerMPMGeneration' in apache:
        table_values.append({
            'key': 'Parent Server MPMGeneration',
            'value': apache['ParentServerMPMGeneration'],
        })
    table_values.append({
        'key': 'Server Name',
        'value': apache['ServerName'],
    })
    table_values.append({
        'key': 'Server MPM',
        'value': apache['ServerMPM'],
    })
    table_values.append({
        'key': 'Server Version',
        'value': apache['ServerVersion'],
    })
    table_values.append({
        'key': 'Server Built',
        'value': apache['Server Built'],
    })

    msg = msg + '\n\n'
    msg += lib.base.get_table(
        table_values,
        ['key', 'value'],
        header=['Key', 'Value'],
        strip=False,
    )

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