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/util.py

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

ngxconf.util
Shared utility functions

Copyright (c) 2018-2020 InMotion Hosting, Inc.
http://www.inmotionhosting.com/

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

"""

import os
import sys
import json
import traceback
import re
import subprocess
import hashlib
import logging

import requests
import yaml
from netaddr import IPAddress
from yaml import CLoader
from jinja2 import Environment

from ngxconf import __version__, __date__, default_conf, gconf_defaults

# disable warnings
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

g_profiles = {}
CP_TASK_QUEUE = "/var/cpanel/taskqueue/servers_queue.json"
CP_QUEUE_CMD = "/usr/local/cpanel/bin/servers_queue"


class GConf(object):
    """
    Global configuration
    """
    _conf = {}

    def __init__(self, cpath='/opt/ngxconf/config.yaml'):
        self.configpath = cpath
        self._conf = self.parse_config(cpath)

    def __getattr__(self, val):
        if val in self._conf:
            return self._conf.get(val)
        else:
            raise KeyError(val)

    def __getitem__(self, val):
        return self._conf.get(val)

    def default(self, val):
        gconf_defaults.get(val)

    def parse_config(self, cpath):
        """
        Parse Ngxconf config
        """
        try:
            with open(cpath, 'r') as f:
                conf = yaml.load(f, Loader=CLoader)
            logger.debug("Loaded global config from %s", cpath)
        except Exception as e:
            logger.warning("Failed to load configuration file: %s", str(e))
            conf = gconf_defaults
        gconf_defaults.update(conf)
        return gconf_defaults

    def __repr__(self):
        return "<GConf: " + str(self._conf) + ">"

class DomainCache(object):
    """
    Domain list object
    """
    domlist = {}

    def __init__(self, domfile="/etc/userdomains"):
        try:
            with open(domfile) as f:
                for tline in f.readlines():
                    try:
                        k, v = tline.split(': ', 1)
                        self.domlist[k.strip()] = v.strip()
                    except:
                        continue
        except:
            logger.error("Failed to read domain list from %s", domfile)

    def exists(self, dom):
        """
        Determine if a domain exists
        """
        return bool(self.domlist.get(dom))

class WHMAPI(object):
    """
    WHM/cPanel API connector
    """
    _rq = None
    _user = 'root'
    _hash = None
    prefix = None

    def __init__(self, server):
        # Grab root accesshash; gen if doesn't exist
        if not self._getRootHash():
            if not self._genHash():
                logger.error("Unable to retrieve WHMAPI credentials; giving up")
                return
        # Create requests session
        self._rq = requests.Session()
        self._rq.headers = {'Authorization': "WHM %s:%s" % (self._user, self._hash)}
        self._rq.verify = False
        self.prefix = "https://%s:2087/json-api" % (server)

    def whm(self, cmd, **kwargs):
        """run whmapi1 command"""
        try:
            resp = self._rq.get(self.prefix+'/'+cmd, params=kwargs)
        except Exception as e:
            logger.error("whmapi request [%s] failed: %s", self.prefix+'/'+cmd, str(e))
            return None

        try:
            rjson = resp.json()
        except:
            rjson = None
        return rjson

    def cpanel(self, mod, cmd, user, **kwargs):
        """run cpapi2 command"""
        cargs = kwargs
        cargs.update({'cpanel_jsonapi_user': user, 'cpanel_jsonapi_apiversion': "2",
                      'cpanel_jsonapi_module': mod, 'cpanel_jsonapi_func': cmd})
        try:
            resp = self._rq.get(self.prefix + '/cpanel', params=cargs)
        except Exception as e:
            logger.error("cpapi2 request [%s] failed: %s", self.prefix+'/cpanel', str(e))
            return None

        try:
            rjson = resp.json()
            rdata = rjson['cpanelresult']['data']
        except:
            rdata = None
        return rdata

    def _getRootHash(self, hashfile='/root/.accesshash'):
        """
        Retrieve root hash (requires this script to be running with effective root permissions)
        """
        try:
            with open(hashfile) as ahf:
                hashraw = ahf.read()
            self._hash = hashraw.replace('\n', '')
            return self._hash
        except Exception as e:
            logger.error("Failed to read root accesshash: %s", str(e))
            return None

    def _genHash(self, hashfile='/root/.accesshash'):
        """
        Generate access hash. 29 lines x 32 chars, first line empty (newline only). Grab 1Kbyte
        from /dev/urandom and generate an MD5 hash (32 chars) * 29 times
        """
        # pylint: disable=unused-variable
        hashout = ''
        for ll in range(1, 30):
            hashout += '\n' + hashlib.md5(os.urandom(1024)).hexdigest()
        try:
            with open(hashfile, 'w') as f:
                f.write(hashout)
            os.chmod(hashfile, 0o0600)
            logger.warning("New root access hash generated OK")
            self._hash = hashout
            return hashout
        except Exception as e:
            logger.error("Failed to generate new root accesshash: %s", str(e))
            return None


logger = logging.getLogger('ngxconf')
gconf = GConf()
whm = None
pkgdata = None

def load_template(tname, basepath=None):
    """
    Load template @basepath/@tname.j2; return template object
    """
    if basepath is None:
        basepath = gconf.template_basepath
    tpath = "%s/%s.j2" % (basepath, tname)

    # Add custom filters
    tenv = Environment(trim_blocks=True, lstrip_blocks=True)
    def _ngx_safe_regex(instr, fallback):
        try:
            re.compile(instr)
            return instr
        except Exception as e:
            try:
                fbstr = '|'.join(default_conf[fallback])
            except:
                fbstr = 'INVALID_REGEX_AND_FALLBACK'
            logger.warning("Invalid user-generated regex (%s): '%s' -- falling back to '%s'", str(e), instr, fbstr)
            return fbstr

    def _ngx_regex_is_valid(instr):
        try:
            re.compile(instr)
            return True
        except Exception as e:
            logger.warning("Invalid user-generated regex (%s): '%s'", str(e), instr)
            return False

    def _ipaddr(instr):
        try:
            IPAddress(instr)
            return True
        except Exception as e:
            return False

    tenv.filters['ngx_safe_regex'] = _ngx_safe_regex
    tenv.filters['ngx_regex_is_valid'] = _ngx_regex_is_valid
    tenv.globals['valid_ipaddr'] = _ipaddr

    # Load template from file
    try:
        with open(tpath, 'r') as f:
            traw = f.read()
    except Exception as e:
        logger.error("Failed to read template from %s: %s", tpath, str(e))
        return None

    # Parse with Jinja2
    try:
        templ = tenv.from_string(traw)
        logger.debug("Parsed template '%s' from file %s", tname, tpath)
    except Exception as e:
        logger.error("Failed to parse template %s: %s", tname, str(e))
        return None

    return templ

def parse_user_default_conf(cpath='/opt/ngxconf/defaults.yaml'):
    """
    Parse user default config file
    Requires `apply_user_default_config: true` in global conf
    """
    try:
        with open(cpath, 'r') as f:
            conf = yaml.load(f, Loader=CLoader)
        logger.debug("Loaded user default config from %s", cpath)
    except Exception as e:
        logger.warning("Failed to load user default config file: %s", str(e))
        conf = None
    return conf

def parse_profiles(basepath="/opt/ngxconf/profiles"):
    """
    Parses all profiles located at @basepath
    """
    global g_profiles

    try:
        plist = [x for x in os.listdir(basepath) if x.endswith('.yaml')]
    except Exception as e:
        logger.error("Failed to read profile directory: %s", str(e))
        return False

    for tpath in plist:
        trealpath = os.path.realpath(os.path.join(basepath, tpath))

        try:
            with open(trealpath, 'r') as f:
                tprof = yaml.load(f, Loader=CLoader)
                g_profiles[tprof['id']] = tprof
            logger.debug("Parsed profile: %s [%s]", tprof['id'], trealpath)
        except Exception as e:
            logger.error("Failed to parse profile %s: %s", trealpath, str(e))

    return True

def get_profile(prof_id):
    """
    Generates a combined profile for @prof_id
    Returns a dict object
    """
    global g_profiles

    if prof_id not in g_profiles:
        return None

    tprof = g_profiles[prof_id]

    if tprof.get('extends'):
        pprofile = get_profile(tprof['extends'])
    else:
        pprofile = default_conf

    pprofile.update(tprof['config'])
    return pprofile

def get_cp_queued_tasks(command):
    """
    Reads from cPanel's task queue JSON file, and
    returns a list of any pending actions for @command
    """
    try:
        with open(CP_TASK_QUEUE) as f:
            rawq = json.load(f)[2]
    except Exception as e:
        logger.error("get_cp_queued_tasks: failed to read servers_queue.json: %s", str(e))
        return None

    tasks = []

    try:
        for ttask in rawq.get('waiting_queue', []) + rawq.get('deferral_queue', []):
            if ttask.get('_command') == command:
                tasks.append(ttask.get('_uuid'))
    except Exception as e:
        logger.error("get_cp_queued_tasks: failed to parse queue data: %s", str(e))
        return None

    return tasks

def cancel_cp_pending_tasks(command):
    """
    Cancel any cPanel tasks pending for @command
    Returns number of tasks de-queued
    """
    tkilled = 0
    tasks = get_cp_queued_tasks(command)
    if tasks is None:
        return 0

    for ttask in tasks:
        try:
            ret = subprocess.run(
                [CP_QUEUE_CMD, 'unqueue', ttask],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                timeout=10,
            )
        except subprocess.TimeoutExpired:
            pass
        if ret.returncode != 0:
            logger.error("cancel_cp_pending_tasks: failed to de-queue task %s (return code %d)", ttask, ret.returncode)
        else:
            tkilled += 1
            logger.warning("cancel_cp_pending_tasks: de-queued %s task %s", command, ttask)

    return tkilled

def get_min_uid():
    """
    Return minimum UID for current system
    """
    try:
        with open('/etc/login.defs') as f:
            llines = f.readlines()
    except Exception as e:
        logger.error("Failed to read login.defs: %s", str(e))
        return None

    ldefs = {}
    for tline in llines:
        try:
            var, val = re.match(r'^(?P<var>[A-Z_0-9]+)\s+(?P<val>.+)$', tline).groups()
            ldefs[var] = val
        except:
            continue

    uid_min = int(ldefs.get('UID_MIN', '1000'))
    logger.debug("Detected UID_MIN = %d", uid_min)
    return uid_min

def cp_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 cp_get_userinfo(username):
    """
    Parse file from /var/cpanel/users/@username, injects [_mtime] field from stat result
    and return a dict of values or None on error
    """
    cpath = os.path.join('/var/cpanel/users', username)
    try:
        with open(cpath) as f:
            cdata = f.readlines()
    except Exception as e:
        logger.error("Failed to read %s: %s", cpath, str(e))
        return None

    udata = {}
    for tline in cdata:
        try:
            if tline.startswith('#'): continue
            k, v = tline.strip().split('=', 1)
            udata[k.strip()] = v.strip()
        except:
            continue
    try:
        udata['_mtime'] = int(os.stat(cpath).st_mtime)
    except:
        udata['_mtime'] = None

    return udata

def dict_strip_mtime(idata):
    """
    Strip _mtime from dict
    """
    return {x: y for x, y in idata.items() if x != '_mtime'}

def excepthook(etype, evalue, etraceback):
    """
    Default exception hook
    """
    logger.critical("Unhandled exception caught:\n%s",
                    '\n'.join(traceback.format_exception(etype, evalue, etraceback)))


Zerion Mini Shell 1.0