Mini Shell

Direktori : /opt/sharedrads/
Upload File :
Current File : //opt/sharedrads/check_apache

#!/opt/imh-python/bin/python3
# vim: set ts=4 sw=4 expandtab syntax=python:
"""RADS tool to parse and display the output of the Apache server status page"""


import sys
import re
import os
import argparse
import requests
from requests import ConnectionError as ConnError, HTTPError
from netaddr import IPAddress, IPNetwork, AddrFormatError


def parse_page(itext):
    """parse HTML from status page and return a list of connections"""
    # strip out all HTML tags; use <td> as vertical-bar delimiter,
    # <tr> as newlines
    itext = (
        re.sub(r'<td[^>]*>', '|', itext).replace('\n', '').replace('<tr>', '\n')
    )
    itext = re.sub(r'<\/?[^>]+>', '', itext).replace('&amp;', '&')

    outbuf = []
    for tline in itext.splitlines():
        if tline[0] != '|':
            continue
        tline = tline[1:]
        # srv, pid, acc, m, cpu, ss, req, (dur), conn, child, slot, client,
        # (proto), vhost, method uri proto
        # proto and dur sections are not present on some machines, so we get
        # 13 - 15 columns
        line = tline.strip().split('|')
        if len(line) < 13:
            continue
        if len(line) == 14:
            del line[11]
        elif len(line) > 14:
            del line[7]
            del line[11]
        htmatch = re.match(r'^([^ ]+) ?(.*) (HTTP.*)$', line[12], re.I)
        if htmatch:
            hmeth, huri, htype = htmatch.groups()
        else:
            hmeth, huri, htype = ('', '', '')
        outbuf.append(
            {
                'srv': line[0],
                'pid': line[1],
                'acc': line[2],
                'm': line[3],
                'cpu': line[4],
                'ss': line[5],
                'req': line[6],
                'conn': line[7],
                'child': line[8],
                'slot': line[9],
                'client': line[10],
                'vhost': line[11],
                'uri': huri,
                'htype': htype,
                'method': hmeth,
            }
        )
    return outbuf


def get_status_page(url="http://localhost/whm-server-status-imh"):
    """retrieve server status page from @url"""
    try:
        resp = requests.get(url, timeout=10.0)
        resp.raise_for_status()
    except ConnError as e:
        print(f"!! Failed to retrieve status page ({url}): {e}")
        sys.exit(100)
    except HTTPError as e:
        print("!! Received error from server: %s" % (e))
        sys.exit(101)
    return resp.text


def parse_status(intext, full=False):
    """parse and format status from HTML"""
    # pick scoreboard header
    if full is True:
        gtext = intext
    else:
        gtext = re.search(r'<body>(.+)<(/p|p /)>', intext, re.S).group(1)
    # strip out HTML tags
    otext = re.sub(r'<td[^>]*>', ' ', gtext)
    otext = re.sub(r'<\/?[^>]+>', '', otext)
    return otext


def enum_domains(indata, ipmask=None):
    """produce a dict of connections per domain"""
    # create mask
    if ipmask is not None:
        try:
            netmask = IPNetwork(ipmask)
        except AddrFormatError as e:
            print("ERROR: Failed to parse netmask: " + str(e))
            netmask = IPNetwork('0/0')
    else:
        netmask = IPNetwork('0/0')

    odata = {}
    for tline in indata:
        # parse vhost & port
        if tline['vhost'].find(':') > 0:
            vhost, vport = tline['vhost'].split(':', 1)
            vhost = vhost.lower()
        else:
            vhost = tline['vhost'].lower()
            vport = '80'

        # parse client IP
        try:
            tclient = IPAddress(tline['client'])
        except AddrFormatError:
            continue

        # match against netmask
        if tclient not in netmask:
            continue

        # build entry
        if vhost in odata:
            odata[vhost]['count'] += 1
            if tclient in odata[vhost]['ips']:
                odata[vhost]['ips'][tclient] += 1
            else:
                odata[vhost]['ips'][tclient] = 1
        else:
            odata[vhost] = {
                'count': 1,
                'domain': vhost,
                'port': vport,
                'ips': {tclient: 1},
            }
    return odata


def enum_ips(indata, ipmask=None):
    """produce a dict of connections per IP"""
    # create mask
    if ipmask is not None:
        try:
            netmask = IPNetwork(ipmask)
        except AddrFormatError as e:
            print("ERROR: Failed to parse netmask: " + str(e))
            netmask = IPNetwork('0/0')
    else:
        netmask = IPNetwork('0/0')

    odata = {}
    for tline in indata:
        # parse vhost & port
        if tline['vhost'].find(':') > 0:
            vhost, vport = tline['vhost'].split(':', 1)
            vhost = vhost.lower()
        else:
            vhost = tline['vhost'].lower()
            vport = '80'

        # parse client IP
        try:
            tclient = IPAddress(tline['client'])
        except AddrFormatError:
            continue

        # match against netmask
        if tclient not in netmask:
            continue

        # build entry
        if tclient in odata:
            odata[tclient]['count'] += 1
            odata[tclient]['ip'] = tclient
            if vhost in odata[tclient]['domains']:
                odata[tclient]['domains'][vhost] += 1
            else:
                odata[tclient]['domains'][vhost] = 1
        else:
            odata[tclient] = {
                'count': 1,
                'client': tclient,
                'port': vport,
                'domains': {vhost: 1},
            }

    return odata


def format_output(indata, sortby='count', nototal=False):
    """perform final processing, then output data"""
    # sort
    sortlist = sorted(indata, key=lambda x: indata[x][sortby])

    # display
    tcount = 0
    for tkey in sortlist:
        tval = indata[tkey]
        if tkey == 'key':
            continue
        tcount += tval['count']
        print("{:6} {}".format(tval['count'], tkey))

    if nototal is False:
        print(f"{tcount:6} TOTAL")


def parse_args():
    """parse CLI args; .mode will contain the operation mode, .ip is an optional
    IP or CIDR range"""
    aparser = argparse.ArgumentParser(
        description="Parse the output of the Apache status page and "
        "process the results"
    )
    aparser.set_defaults(mode=None, ip=None, nototal=False)
    aparser.add_argument(
        '--status',
        action='store_const',
        dest='mode',
        const='status',
        help="Show Apache scoreboard",
    )
    aparser.add_argument(
        '--statusfull',
        action='store_const',
        dest='mode',
        const='statusfull',
        help="Show full Apache status output",
    )
    aparser.add_argument(
        '--countdomain',
        action='store_const',
        dest='mode',
        const='countdomain',
        help="Count number of connections per domain/VHost",
    )
    aparser.add_argument(
        '--sortdomain',
        action='store_const',
        dest='mode',
        const='sortdomain',
        help="Sort domains/VHosts by number of connections",
    )
    aparser.add_argument(
        '--countip',
        action='store_const',
        dest='mode',
        const='countip',
        help="Count number of connections by IP",
    )
    aparser.add_argument(
        '--sortip',
        action='store_const',
        dest='mode',
        const='sortip',
        help="Sort IPs by number of connections",
    )
    aparser.add_argument(
        '--ipmask',
        dest='ip',
        metavar="IP/CIDR",
        help="Filter by a specific IP address or CIDR range",
    )
    aparser.add_argument(
        '--nototal',
        dest='nototal',
        action='store_true',
        default=False,
        help="Suppress total output",
    )

    args = aparser.parse_args(sys.argv[1:])
    if args.mode is None:
        aparser.print_help()
        sys.exit(250)
    else:
        return args


def _main():
    """entry point"""
    args = parse_args()
    if not 'sharedrads' in os.path.realpath(__file__):
        url = 'http://localhost/whm-server-status'
    else:
        url = 'http://localhost/whm-server-status-imh'
    spage = get_status_page(url)
    sdata = parse_page(spage)
    sortby = None

    if args.mode.startswith('status'):
        if args.mode == 'statusfull':
            sbtext = parse_status(spage, full=True)
        else:
            sbtext = parse_status(spage)
        print(sbtext)
    elif args.mode == 'countdomain':
        fdata = enum_domains(sdata, ipmask=args.ip)
        sortby = 'count'
    elif args.mode == 'sortdomain':
        fdata = enum_domains(sdata, ipmask=args.ip)
        sortby = 'domain'
    elif args.mode == 'countip':
        fdata = enum_ips(sdata, ipmask=args.ip)
        sortby = 'count'
    elif args.mode == 'sortip':
        fdata = enum_ips(sdata, ipmask=args.ip)
        sortby = 'client'
    else:
        print("!! No valid action selected.")
        sys.exit(251)

    # display sorted output
    if sortby is not None:
        format_output(fdata, sortby, nototal=args.nototal)


if __name__ == '__main__':
    _main()

Zerion Mini Shell 1.0