Mini Shell

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

# 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