Mini Shell
# 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