Mini Shell

Direktori : /opt/imh-python/lib/python3.9/site-packages/fpmstatus/
Upload File :
Current File : //opt/imh-python/lib/python3.9/site-packages/fpmstatus/fpm.py

# vim: set ts=4 sw=4 expandtab syntax=python:
"""

fpmstatus.fpm
FPM pool status parser

@author J. Hipps <jacobh@inmotionhosting.com>

"""

import os
import logging
import json
import socket
from glob import glob
from configparser import SafeConfigParser
from urllib.parse import splitport
from pathlib import Path

import arrow

from fpmstatus.util import *
from fpmstatus.fcgi import FCGIApp
from fpmstatus import __version__

logger = logging.getLogger('fpmstatus')


def fetch_pools(phpver=None):
    """
    Fetch list of pools
    If @phpver is defined, only return EA4 pools for that version
    """
    logger.info("Enumerating pools for all FPM masters...")

    conftype = None
    if os.path.exists('/etc/php/fpm/pool.d'):
        # Standard Debian
        pcs = glob('/etc/php/fpm/pool.d/*.conf')
        conftype = 'debian'
        logger.debug("Got %d pools from /etc/php/fpm/pool.d", len(pcs))
    elif os.path.exists('/etc/php-fpm.d'):
        # Standard RHEL
        pcs = glob('/etc/php-fpm.d/*.conf')
        conftype = 'rhel'
        logger.debug("Got %d pools from /etc/php-fpm.d", len(pcs))
    elif os.path.exists('/etc/systemd/system/supervisord-fpm.service'):
        # Ngxconf supervisord-fpm
        pcs = glob('/opt/ngxconf/phpfpm/conf.d/*.conf')
        conftype = 'ngxconf'
        logger.debug("Got %d pools from /opt/ngxconf/phpfpm/conf.d", len(pcs))
    elif len(glob('/opt/cpanel/ea-php*/root/etc/php-fpm.d')):
        # cPanel EA4
        conftype = 'cpanel'
        if phpver:
            pcs = glob('/opt/cpanel/%s/root/etc/php-fpm.d/*.conf' % (phpver))
            logger.debug("Got %d pools for %s", len(pcs), phpver)
        else:
            pcs = glob('/opt/cpanel/*/root/etc/php-fpm.d/*.conf')
            logger.debug("Got %d pools for all PHP versions", len(pcs))
    elif len(glob('/opt/alt/php-fpm*/usr/etc/php-fpm.d/users/*.conf')):
        # CWP
        conftype = 'cwp'
        if phpver:
            pcs = glob('/opt/alt/%s/usr/etc/php-fpm.d/users/*.conf' % (phpver))
            logger.debug("Got %d pools for %s", len(pcs), phpver)
        else:
            pcs = glob('/opt/alt/php-fpm*/usr/etc/php-fpm.d/users/*.conf')
            logger.debug("Got %d pools for all PHP versions", len(pcs))
    else:
        logger.error("No FPM installation detected. Aborting.")
        return None

    if len(pcs) == 0:
        logger.error("No FPM pools detected. Aborting.")
        return None
    

    tglobal = {}
    pools = {}
    for tconf in pcs:
        cparse = SafeConfigParser()
        if tconf.endswith('/nobody.conf'): # prevents enumeration of the nobody pool in CWP
            continue
        try:
            with open(tconf, 'r') as f:
                cparse.readfp(f)
        except Exception as e:
            logger.error("Failed to parse %s: %s", tconf, str(e))
            continue

        for tsec in cparse.sections():
            if tsec == 'global':
                tglobal = dict(cparse.items('global'))
            else:
                pools[tsec] = dict(cparse.items(tsec))
                pools[tsec]['_confpath'] = tconf
                pools[tsec]['_vhost'] = tsec.replace('_', '.')

                if conftype == 'ngxconf':
                    try:
                        pools[tsec]['_masterid'] = os.path.splitext(os.path.basename(tconf))[0]
                        pools[tsec]['_masterlog'] = tglobal.get('error_log')
                    except:
                        pools[tsec]['_masterid'] = None
                        pools[tsec]['_masterlog'] = None
                        pass
                logger.debug("Found pool %s [listen=%s]", tsec, pools[tsec].get('listen'))

    return pools

def get_pool_status(psock, uri='/status', timeout=1.0):
    """
    Get pool status from @socket via FastCGI
    """
    socket.setdefaulttimeout(timeout)

    # Determine if psock is unix or tcp socket
    try:
        shost, sport = splitport(psock)
    except Exception as e:
        logger.error("Failed to parse socket path [%s]: %s", psock, str(e))
        return None

    try:
        if sport is not None:
            fc = FCGIApp(host=shost, port=int(sport))
        else:
            fc = FCGIApp(psock)
        fenv = {
            'REQUEST_METHOD': "GET",
            'REQUEST_URI': uri,
            'SCRIPT_NAME': uri,
            'SCRIPT_FILENAME': uri,
            'QUERY_STRING': "full&json",
            'DOCUMENT_ROOT': "/",
            'GATEWAY_INTERFACE': "CGI/1.1",
            'SERVER_SOFTWARE': "fpmstatus/" + __version__,
            'REMOTE_ADDR': "127.0.0.1",
            'REMOTE_PORT': "0",
            'SERVER_ADDR': "127.0.0.1",
            'SERVER_PORT': "0",
            'SERVER_NAME': "localhost",
            'CONTENT_TYPE': "",
            'CONTENT_LENGTH': "0"
        }
        resp = fc(fenv)
    except Exception as e:
        logger.error("Failed to read from socket %s: %s", psock, str(e))
        return None

    pstat = {}
    try:
        if resp[2].startswith(b'File not found'):
            logger.error("Received 404 when requesting /status. Check FPM pool configuration for %s", psock)
            return None
        sraw = json.loads(resp[2])
    except Exception as e:
        logger.error("Failed to parse JSON response from %s:%s: %s", psock, uri, str(e))
        return None

    for tkey, tval in sraw.items():
        nkey = tkey.replace(' ', '_')

        if nkey == 'start_time':
            pstat[nkey] = arrow.get(tval).to('local').int_timestamp
        elif nkey == 'processes':
            procs = tval
            pstat[nkey] = []
            for tproc in procs:
                tpx = {}
                for pkey, pval in tproc.items():
                    npkey = pkey.replace(' ', '_')
                    if npkey == 'script':
                        tpx[npkey] = None if pval == '-' else os.path.realpath(pval)
                    elif npkey == 'start_time':
                        tpx[npkey] = arrow.get(pval).to('local').int_timestamp
                    elif npkey == 'request_duration':
                        tpx[npkey] = float(pval) / 1000000.0
                    else:
                        tpx[npkey] = pval
                pstat[nkey].append(tpx)
        else:
            pstat[nkey] = tval

    logger.debug("Got repsonse for pool %s:\n%s", psock, pstat)
    return pstat

def get_domain_pool(domname):
    """
    Get pool by domain @domname
    """
    poolname = domname.replace('.', '_')

    plist = fetch_pools()
    logger.info("Fetching status of pool %s...", poolname)

    pdata = plist.get(poolname)
    if pdata is None:
        logger.error("No pool for domain %s found on server", domname)
        return None
    pstat = get_pool_status(pdata['listen'])

    if pstat is None:
        logger.error("Failed to retrieve pool status for %s", domname)
        return None
    else:
        pstat['_config'] = pdata
        return pstat

def get_user_pools(username):
    """
    Get all pools for user @username
    """
    ustat = []
    plist = fetch_pools()
    logger.info("Fetching status of pools owned by %s...", username)

    for tpool in filter(lambda x: x.get('user', '') == username, plist.values()):
        pstat = get_pool_status(tpool['listen'])
        if pstat is None:
            logger.warning("Failed to retrieve pool status for %s", tpool.get('pool', '<unknown>'))
            continue
        else:
            pstat['_config'] = tpool
            ustat.append(pstat)

    logger.debug("Got %d pools for user %s", len(ustat), username)
    return ustat

def get_pool_by_name(poolname):
    """
    Get pool named @poolname
    """
    plist = fetch_pools()
    logger.info("Fetching status of %s pool...", poolname)
    tpool = plist.get(poolname)

    if tpool is None:
        logger.error("No pool named '%s'", poolname)
        return None

    pstat = get_pool_status(tpool['listen'])
    if pstat is None:
        logger.warning("Failed to retrieve pool status for %s", poolname)
        return None
    else:
        pstat['_config'] = tpool
        return pstat

def get_all_pools():
    """
    Fetch status for ALL pools on the server
    """
    ustat = []
    plist = fetch_pools()
    if not plist:
        logger.error("No PHP-FPM pools found on server")
        return None

    logger.info("Fetching status of all %d pools...", len(plist))
    for pname, tpool in plist.items():
        pstat = get_pool_status(tpool['listen'])
        if pstat is None:
            logger.warning("Failed to retrieve pool status for %s", tpool.get('pool', '<unknown>'))
            continue
        else:
            pstat['_config'] = tpool
            ustat.append(pstat)
    return ustat

Zerion Mini Shell 1.0