Mini Shell
# vim: set ts=4 sw=4 expandtab syntax=python:
"""
ngxconf.cpfpm
cPanel PHP-FPM Interface
cPanel FPM system with master-per-version schema
Copyright (c) 2017-2020 InMotion Hosting, Inc.
http://www.inmotionhosting.com/
@author J. Hipps <jacobh@inmotionhosting.com>
"""
# pylint: disable=invalid-name
import os
import re
import hashlib
import logging
from glob import glob
import requests
import yaml
from yaml import CLoader, CDumper
from fpmstatus.fpm import fetch_pools
from ngxconf.util import WHMAPI, gconf
logger = logging.getLogger('ngxconf')
whm = None
phpvers = None
pkgdata = None
userlist = None
default_phpver = None
udom_map = {}
def kill_orphans():
"""
Parse all PHP-FPM pool config files and check that the chdir path exists.
If not, the config file is renamed with a `.orphaned` suffix.
"""
stat = {'tot': 0, 'orphans': 0, 'errors': 0}
logger.debug("Checking for orphaned pool configuration files...")
plist = fetch_pools()
if not plist:
logger.warning("No pools detected")
return
for pname, tpool in plist.items():
stat['tot'] += 1
try:
if not os.path.exists(tpool.get('chdir', '/')):
newpath = tpool['_confpath'] + '.orphaned'
os.rename(tpool['_confpath'], newpath)
logger.warning("Orphaned pool for domain %s: %s -> %s", tpool['_vhost'], tpool['_confpath'], newpath)
stat['orphans'] += 1
except Exception as e:
stat['errors'] += 1
logger.error("Failed to fix orphaned pool [%s] at %s: %s", tpool['_vhost'], tpool['_confphat'], str(e))
continue
logger.debug("%d total pools, %d orphans fixed, %d errors", stat['tot'], stat['orphans'], stat['errors'])
def update_fpm(user, vlist, force=False):
"""
Ensure PHP-FPM is enabled for all domains on user's account
"""
# Establish new connection, otherwise, reuse existing connection
global whm
if whm is None:
whm = WHMAPI('localhost')
udata = get_user_data(user)
upkg = get_package_data(udata.get('plan'))
# Find existing PHP-FPM configs
existing_conf = glob("/var/cpanel/userdata/%s/*.php-fpm.yaml" % (user))
actual_conf = []
tupdate = []
for tdom in vlist:
realdom = get_real_domain(user, tdom)
tver = get_user_php_version(user, realdom)
tver_norm = xlate_phpver(tver)
fyaml = "/var/cpanel/userdata/%s/%s.php-fpm.yaml" % (user, realdom)
fconf = "/opt/cpanel/%s/root/etc/php-fpm.d/%s.conf" % (tver_norm, realdom)
cpconf = "/var/cpanel/users/%s" % (user)
actual_conf.append(fyaml)
# Check mtime of conf files
try:
ytime = os.stat(fyaml).st_mtime
except:
ytime = 0
try:
ctime = os.stat(fconf).st_mtime
except:
ctime = 0
try:
cptime = os.stat(cpconf).st_mtime
except:
cptime = 0
if force or ytime > ctime or cptime > ytime or 0 in (ytime, ctime, cptime):
if gconf.enforce_fpm_limits:
tcall = {
'_is_present': 1,
'pm_max_children': int(upkg.get('IMH_FPM_WORKERS', 5)),
'pm_process_idle_timeout': int(upkg.get('IMH_FPM_IDLE', 60)),
'pm_max_requests': int(upkg.get('IMH_FPM_MAXREQ', 128)),
'rlimit_files': gconf.fpm_worker_rlimit_files,
}
else:
try:
with open(fyaml, 'r') as f:
tcall = yaml.load(f, Loader=CLoader)
except:
tcall = {}
tcall.update({'_is_present': 1})
try:
with open(fyaml, 'w') as f:
f.write('---\n')
yaml.dump(tcall, stream=f, Dumper=CDumper, default_flow_style=False)
tupdate.append(realdom)
logger.debug("Wrote updated PHP-FPM limits for %s:%s", user, realdom)
except Exception as e:
logger.error("Failed to write updated PHP-FPM config [%s:%s]: %s", user, realdom, str(e))
else:
logger.debug("PHP-FPM config for %s:%s is up-to-date", user, tdom)
# Purge any domains that have been removed
leftovers = set(existing_conf) - set(actual_conf)
if len(leftovers):
for tconf in leftovers:
try:
os.unlink(tconf)
os.unlink(re.sub(r'\.yaml$', '.cache', tconf))
logger.info("Removed stale PHP-FPM config file: %s", tconf)
except Exception as e:
logger.error("Failed to remove stale PHP-FPM config file: %s", str(e))
return tupdate
def get_user_php_version(user, vhost):
"""
Return selected PHP version for user/vhost
On first run, queries whmapi1 for a list of all selected versions
If the version cannot be determined, 'inherit' is returned
"""
global whm
global phpvers
if whm is None:
whm = WHMAPI('localhost')
if phpvers is None:
# Query WHMAPI1 for list of all user version selections
rargs = {'api.version': 1}
rout = whm.whm('php_get_vhost_versions', **rargs)
if rout.get('data') and rout['data'].get('versions'):
phpvers = {}
try:
for thost in rout['data']['versions']:
if thost['account'] not in phpvers:
phpvers[thost['account']] = {thost['vhost']: thost['version']}
else:
phpvers[thost['account']][thost['vhost']] = thost['version']
except Exception as e:
logger.error("Failed to enumerate user PHP version selections: %s", str(e))
return 'inherit'
else:
logger.error("Failed to enumerate user PHP version selections: %s", rout)
phpvers = {}
return 'inherit'
if not phpvers.get(user):
logger.warning("get_user_php_version: no matching user")
return 'inherit'
elif not phpvers[user].get(vhost):
logger.warning("get_user_php_version: no matching vhost")
return 'inherit'
else:
return phpvers[user][vhost]
def get_package_data(pkg):
"""
Return info for specified package
Retrieves a list of packages and package limits, then caches it
"""
global whm
global pkgdata
if whm is None:
whm = WHMAPI('localhost')
if pkgdata is None:
pkgdata = {}
packraw = whm.whm('listpkgs').get('package')
if packraw:
for tpack in packraw:
pkgdata[tpack['name']] = tpack
else:
logger.error("Failed to retrieve list of packages; packraw = %s", packraw)
return {}
if not pkgdata.get(pkg):
logger.warning("get_package_data: no matching package '%s'", pkg)
return {}
else:
return pkgdata.get(pkg)
def get_user_data(user):
"""
Returns listacct data for @user
Runs listaccts for all users, then caches that data
"""
global whm
global userlist
if whm is None:
whm = WHMAPI('localhost')
if userlist is None:
userlist = {}
uraw = whm.whm('listaccts').get('acct')
if uraw:
for tuser in uraw:
try:
userlist[tuser['user']] = tuser
except Exception as e:
logger.warning("Failed to parse acct data: %s", str(e))
else:
logger.error("Failed to retrieve list of users; uraw = %s", uraw)
return {}
if not userlist.get(user):
logger.warning("get_user_data: no matching user '%s'", user)
return {}
else:
return userlist.get(user)
def get_real_domain(user, dom):
"""
Determine 'real' domain name, return as string
PHP-FPM uses the actual domain name (eg. addon.com) as opposed to
the virtualhost name (eg. addon.primary.com) like cPanel uses for
everything else. Awesome.
"""
# pylint: disable=global-variable-not-assigned
global udom_map
if user not in udom_map:
try:
with open("/var/cpanel/userdata/%s/main" % (user), 'r') as f:
rudata = yaml.load(f, Loader=CLoader)
except Exception as e:
logger.error("Failed to read userdata/main for %s: %s", user, str(e))
udom_map[user] = {}
return dom
try:
udom_map[user] = dict(zip(rudata.get('sub_domains', []), rudata.get('sub_domains', [])))
udom_map[user].update({rudata.get('main_domain', ''): rudata.get('main_domain', '')})
udom_map[user].update(dict(zip(rudata['addon_domains'].values(), rudata['addon_domains'].keys())))
except Exception as e:
logger.error("Failed to parse userdata/main for %s: %s", user, str(e))
udom_map[user] = {}
return udom_map[user].get(dom, dom)
def xlate_phpver(verstr):
"""
Translate PHP version string to normalized version (eg. 'inherit' -> 'ea-php56', etc.)
"""
global whm
global default_phpver
if verstr == 'inherit':
if default_phpver is None:
if whm is None:
whm = WHMAPI('localhost')
try:
api1 = {'api.version': 1}
default_phpver = whm.whm('php_get_system_default_version', **api1)['data']['version']
except Exception as e:
logger.error("Failed to determine default system PHP version: %s", str(e))
logger.warning("Falling back to %s", gconf.fallback_phpver)
default_phpver = gconf.fallback_phpver
return default_phpver
else:
return verstr
Zerion Mini Shell 1.0