D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
clcommon
/
cpapi
/
plugins
/
Filename :
cpanel.py
back
Copy
# -*- coding: utf-8 -*- import json import logging import os import pwd import subprocess import sys import urllib.request import urllib.parse import urllib.error from configparser import ConfigParser, NoOptionError, NoSectionError from collections import OrderedDict import requests from traceback import format_exc from clcommon.cpapi.pluginlib import getuser from urllib.parse import urlparse from clcommon import ClPwd from clcommon.cpapi.cpapiexceptions import DuplicateData, CPAPIExternalProgramFailed, ParsingError, \ EncodingError, NotSupported from clcommon.lib.whmapi_lib import WhmApiRequest, WhmApiError, WhmNoPhpBinariesError from clcommon.utils import run_command, ExternalProgramFailed, grep, get_file_lines from clcommon.clconfpars import load as loadconfig from clcommon.cpapi.cpapiexceptions import NoDBAccessData, CpApiTypeError, NoDomain from clcommon.cpapi.GeneralPanel import ( GeneralPanelPluginV1, CPAPI_CACHE_STORAGE, PHPDescription, DomainDescription ) from clcommon.clconfpars import load_fast __cpname__ = 'cPanel' DBMAPPING_SCRIPT = os.path.join(os.path.dirname(sys.executable), "cpanel-dbmapping") UAPI = '/usr/bin/uapi' logger = logging.getLogger(__name__) # WARN: Probably will be deprecated for our "official" plugins. # See pluginlib.detect_panel_fast() def detect(): return os.path.isfile('/usr/local/cpanel/cpanel') CPANEL_DB_CONF = '/root/.my.cnf' CPANEL_USERPLANS_PATH = '/etc/userplans' CPANEL_DATABASES_PATH = '/var/cpanel/databases/' CPANEL_USERS_DIR = '/var/cpanel/users/' CPANEL_RESELLERS_PATH = '/var/cpanel/resellers' CPANEL_USERDATADOMAINS_PATH = '/etc/userdatadomains;/var/cpanel/userdata/{user}/cache' CPANEL_USERDATAFOLDER_PATH = '/var/cpanel/userdata/{user}' CPANEL_ACCT_CONF_PATH = '/etc/wwwacct.conf' CPANEL_USEROWNERS_FILE = '/etc/trueuserowners' SYSCONF_CLOUDLINUX_PATH = '/etc/sysconfig/cloudlinux' CPANEL_CONFIG = '/var/cpanel/cpanel.config' USERCONF_PARAM_MAP = { 'dns': 'dns', 'package': 'plan', 'reseller': 'owner', 'mail': 'contactemail', 'locale': 'locale', 'cplogin': 'user' } SUPPORTED_CPANEL_CPINFO = ('cplogin', 'package', 'mail', 'reseller', 'dns', 'locale') def db_access(_conf_path=CPANEL_DB_CONF): access = {} reg_data_config = ConfigParser( allow_no_value=True, interpolation=None, strict=False ) opened_files = reg_data_config.read(_conf_path) if not opened_files: raise NoDBAccessData( f"Cannot find database access data for localhost. No such file {_conf_path}" ) # Options in MySQL config files can be double- or single-quoted, so we strip() them try: if reg_data_config.has_option(section="client", option="password"): access["pass"] = reg_data_config.get( section="client", option="password", raw=True ).strip("\"'") else: access["pass"] = reg_data_config.get( section="client", option="pass", raw=True ).strip("\"'") access["login"] = reg_data_config.get( section="client", option="user", raw=True ).strip("\"'") except (NoOptionError, NoSectionError) as err: raise NoDBAccessData( "Cannot find database access data for localhost from config " f"file {_conf_path}; {err.message}" ) from err access["db"] = "mysql" return access def cpusers(_userplans_file=CPANEL_USERPLANS_PATH): """ Parse the file /etc/userplans, which contains the pairs of user-plan :param _userplans_file: path to the user's plans file :return: list of the non-system users """ with open(_userplans_file, encoding='utf-8') as stream: users_list = [line.split(':')[0].strip() for line in stream if not line.startswith('#') and line.count(':') == 1 and len(line.strip()) > 3] return tuple(users_list) def resellers(_resellers_path=CPANEL_RESELLERS_PATH): if not os.path.isfile(_resellers_path): # on a clean system, this file may not be return tuple() with open(_resellers_path, encoding='utf-8') as stream: # Example of file # res1res1:add-pkg,add-pkg-ip,add-pkg-shell # res1root:add-pkg,add-pkg-ip,add-pkg-shell,allow-addoncreate,allow-emaillimits-pkgs # r: resellers_list = [line.split(':', 1)[0].strip() for line in stream if not line.startswith('#') and (':' in line) and len(line.strip()) > 1] return tuple(resellers_list) def admins(): return {'root'} def is_reseller(username, _resellers_path=CPANEL_RESELLERS_PATH): """ Check if given user is reseller; :param _resellers_path: for testing only :type username: str :rtype: bool """ return any(cplogin == username for cplogin in resellers(_resellers_path)) def dblogin_cplogin_pairs(cplogin_lst=None, with_system_users=False): """ Get mapping between system and DB users @param cplogin_lst :list: list with usernames for generate mapping @param with_system_users :bool: add system users to result list or no. default: False """ # initialize results list results = [] # accept only list and tuple parameters uid_list = [] for username in (cplogin_lst or []): try: uid_list.append(str(pwd.getpwnam(username).pw_uid)) except KeyError: # no user exists - skip it uid_list.append("-1") # generate system command params = [DBMAPPING_SCRIPT] if not with_system_users: params.append("--nosys") params += uid_list with subprocess.Popen( params, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) as p: output, _ = p.communicate() # output format: "DBuser user UID" for line in output.split("\n"): line = line.strip() if line: results.append(line.split()[:2]) return tuple(results) def cpinfo(cpuser=None, keyls=('cplogin', 'package', 'mail', 'reseller', 'dns', 'locale'), _cpanel_users_dir=CPANEL_USERS_DIR, quiet=True, search_sys_users=True): returned = [] if isinstance(cpuser, str): cpusers_list = [cpuser] elif isinstance(cpuser, (list, tuple)): cpusers_list = tuple(cpuser) elif cpuser is None: cpusers_list = cpusers(_userplans_file=CPANEL_USERPLANS_PATH) else: raise CpApiTypeError(funcname='cpinfo', supportedtypes='str|unicode|list|tuple', received_type=type(cpuser).__name__) for cpuser in cpusers_list: user_config_path = os.path.join(_cpanel_users_dir, cpuser) if not os.path.exists(user_config_path): if not quiet: sys.stderr.write(f'WARNING: Can not load data to the user "{cpuser}"; ' 'Perhaps there is no such user in cPanel') continue # ignore bad symbols in config here # because of LVEMAN-1150 (which also seems # being already fixed by cpanel) cpuser_data = loadconfig(user_config_path, ignore_bad_encoding=True) user_data = [] for data_key in keyls: data = cpuser_data.get(USERCONF_PARAM_MAP.get(data_key)) # USERCONF_PARAM_MAP.get('cplogin') == user # check if user tag in user config and if tag is missing - use file name as user name if data_key == 'cplogin' and data is None: data = os.path.basename(user_config_path) user_data.append(data) returned.append(tuple(user_data)) if 'mail' in keyls: # checking the presence of an additional e-mail additional_mail = cpuser_data.get('contactemail2') if additional_mail: user_data[list(keyls).index('mail')] = additional_mail user_data_tuple = tuple(user_data) if user_data_tuple not in returned: returned.append(tuple(user_data)) return tuple(returned) def get_admin_email(_conf1=None, _conf2=None, _hostname=None): """ :param str|None _conf1: for testing :param str|None _conf2: for testing :param str|None _hostname: for testing :return: """ # 1. Try to get admin email from /etc/sysconfig/cloudlinux lines = [] try: lines = get_file_lines(_conf1 or SYSCONF_CLOUDLINUX_PATH) except (OSError, IOError): pass for line in lines: if line.startswith('EMAIL'): parts = line.split('=') if len(parts) == 2 and '@' in parts[1].strip(): return parts[1].strip() # 2. Try to get admin email from /etc/wwwacct.conf lines = [] try: lines = get_file_lines(_conf2 or CPANEL_ACCT_CONF_PATH) except (OSError, IOError): pass host = '' for line in lines: if line.startswith('CONTACTEMAIL'): s = line.replace('CONTACTEMAIL', '').strip() if s: return s if line.startswith('HOST'): s = line.replace('HOST', '').strip() if s: host = s if host: return 'root@' + host # Admin email not found in system files, use common address from clcommon.cpapi.plugins.universal import get_admin_email # pylint: disable=import-outside-toplevel return get_admin_email(_hostname=_hostname) def _parse_userdatadomains(_path, parser, quiet=True): if '{user}' in _path: call_as_user = pwd.getpwuid(os.geteuid()).pw_name _path = _path.replace('{user}', call_as_user) path_list = _path.split(';') for path_ in path_list: if not os.path.exists(path_): continue try: with open(path_, encoding='utf-8') as stream: # example line: # test.russianguns.ru: russianguns==root==sub== # russianguns.ru==/home/russianguns/fla==192.168.122.40:80======0 for i, line in enumerate(stream): if not line.strip(): # ignore the empty string continue if line.count(': ') != 1: if not quiet: sys.stderr.write(f'Can\'t parse {i} line in file "{path_}"; line was ignored\n') continue domain, domain_raw_data = line.split(': ') domain_data = domain_raw_data.strip().split('==') parser(path_, domain, domain_data) except IOError as e: if not quiet: sys.stderr.write(f"Can't open file {path_} [{e}]\n") continue def _parse_userdataaliases(_path, quiet=True): path_list = _path.split(';') aliases = [] for path_ in path_list: if not os.path.exists(path_): continue try: with open(path_, encoding='utf-8') as stream: # example line: # test.russianguns.ru: russianguns==root==sub== # russianguns.ru==/home/russianguns/fla==192.168.122.40:80======0 for i, line in enumerate(stream): if not line.strip(): # ignore the empty string continue if "serveralias" not in line: continue aliases += line.replace("serveralias: ", '').strip().split(' ') except IOError as e: if not quiet: sys.stderr.write(f"Can't open file {path_} [{e}]\n") continue return aliases def useraliases(cpuser, domain, _path=CPANEL_USERDATAFOLDER_PATH, quiet=True): # use dict to avoid duplicates if '{user}' in _path: _path = _path.replace('{user}', cpuser) _path = os.path.join(_path, domain) aliases = _parse_userdataaliases(_path, quiet=quiet) return list(aliases) def docroot(domain, _path=CPANEL_USERDATADOMAINS_PATH, quiet=True): domain = domain.strip() pathes = set() result = {'docroot_path': None, 'user': None} def parser(path, d, domain_data): pathes.add(path) if d == domain: result['docroot_path'] = domain_data[4] result['user'] = domain_data[0] _parse_userdatadomains(_path, parser, quiet=quiet) if not (result['docroot_path'] is None or result['user'] is None): return result['docroot_path'], result['user'] watched = '; '.join( ['Can\'t find record "%(d)s" in file "%(p)s"' % {'d': domain, 'p': p} for p in pathes] ) raise NoDomain(f"Can't obtain document root for domain '{domain}'; {watched}") # User name to domain cache # Example: # { 'user1': [('user1.org', '/home/user1/public_html'), # ('mk.user1.com.user1.org', '/home/user1/public_html/www.mk.user1.com')] } _user_to_domains_map_cpanel = {} def userdomains(cpuser, _path=CPANEL_USERDATADOMAINS_PATH, quiet=True): # If user present in cache, take data from it if cpuser in _user_to_domains_map_cpanel: return _user_to_domains_map_cpanel[cpuser] # use dict to avoid duplicates domains_tmp = OrderedDict() domains = OrderedDict() def parser(path, d, domain_data): user_ = domain_data[0] document_root = domain_data[4] # Update cache if user_ in _user_to_domains_map_cpanel: user_data = _user_to_domains_map_cpanel[user_] else: user_data = [] if 'main' == domain_data[2]: # insert main domain to 1st position in list if (d, document_root) not in user_data: user_data.insert(0, (d, document_root)) else: if (d, document_root) not in user_data: user_data.append((d, document_root)) _user_to_domains_map_cpanel[user_] = user_data if user_ == cpuser: if 'main' == domain_data[2]: # main domain must be first in list domains.update({d: document_root}) else: domains_tmp.update({d: document_root}) _parse_userdatadomains(_path, parser, quiet=quiet) domains.update(domains_tmp) return list(domains.items()) def domain_owner(domain, _path=CPANEL_USERDATADOMAINS_PATH, quiet=True): users_list = [] def parser(path, d, domain_data): if d == domain: users_list.append(domain_data[0]) _parse_userdatadomains(_path, parser, quiet=quiet) if len(users_list) > 1: raise DuplicateData( f"domain {domain} belongs to few users: [{','.join(users_list)}]" ) if len(users_list) == 0: return None return users_list[0] def homedirs(_sysusers=None, _conf_path = CPANEL_ACCT_CONF_PATH): """ Detects and returns list of folders contained the home dirs of users of the cPanel :param str|None _sysusers: for testing :param str|None _conf_path: for testing :return: list of folders, which are parent of home dirs of users of the panel """ HOMEDIR = 'HOMEDIR ' HOMEMATCH = 'HOMEMATCH ' homedirs = [] users_homedir = '' users_home_match = '' if os.path.exists(_conf_path): lines = get_file_lines(_conf_path) for line in lines: if line.startswith(HOMEDIR): users_homedir = line.split(HOMEDIR)[1].strip() elif line.startswith(HOMEMATCH): users_home_match = line.split(HOMEMATCH)[1].strip() if users_homedir: homedirs.append(users_homedir) clpwd = ClPwd() users_dict = clpwd.get_user_dict() # for testing only if isinstance(_sysusers, (list, tuple)): class pw: def __init__(self, name, dir): self.pw_name = name self.pw_dir = dir users_dict = {} for (name,dir) in _sysusers: users_dict[name] = pw(name, dir) for user_data in users_dict.values(): userdir = user_data.pw_dir if os.path.exists(userdir + '/public_html') or os.path.exists(userdir + '/www'): homedir = os.path.dirname(userdir) if users_home_match and homedir.find('/'+users_home_match) == -1: continue if homedir not in homedirs: homedirs.append(homedir) return homedirs def _reseller_users_parser(json_string): try: json_serialized = json.loads(json_string) result = json_serialized['result'] return [item['user'] for item in result['data']] except (KeyError, ValueError, TypeError) as e: raise ParsingError(str(e)) from e def _reseller_users_json(reseller_name=None): """ Call UAPI and get json string; :type reseller_name: str | None :raises: ParsingError, CPAPIExternalProgramFailed :rtype: str """ reseller_name = reseller_name or getuser() # Attention!! /usr/bin/uapi utility may works unstable. See PTCLLIB-95 for details cmd = [UAPI, 'Resellers', 'list_accounts', '--output=json'] # root user MUST specify --user, and reseller CAN'T do that if reseller_name != getuser() or getuser() == "root": cmd.append(f'--user={urllib.parse.quote(reseller_name)}') try: json_string = run_command(cmd=cmd, return_full_output=True)[1] # take only std_out, ignore std_err except ExternalProgramFailed as e: raise CPAPIExternalProgramFailed(str(e)) from e return json_string def reseller_users(resellername): """ Return reseller users :param resellername: reseller name; return empty list if None :return list[str]: user names list """ # Attention!! /usr/bin/uapi utility may works unstable. See PTCLLIB-95 for details # json_string = _reseller_users_json(resellername) # return _reseller_users_parser(json_string) # So we read reseller's user list from /etc/trueuserowners # /etc/trueuserowners example: # #userowners v1 # cltest1: root # res: res # res1: root # res2: root # res2user1: res2 # res2usr1: res2 # resnew: resnew # resnew1: resnew1 # rn1user1: resnew1 if resellername is None: return [] result = [] userowner_file_data = get_file_lines(CPANEL_USEROWNERS_FILE) if resellername is not None: for line in grep(rf'\: {resellername}$', fixed_string=False, match_any_position=True, multiple_search=True, data_from_file=userowner_file_data): splitted_line = line.strip().split(': ') result.append(splitted_line[0]) return result def _reseller_user_domains_parser(json_string): try: json_serialized = json.loads(json_string) result = json_serialized['result'] users_data = {} for item in result['data']: users_data[item['user']] = item['domain'] return users_data except (KeyError, ValueError, TypeError) as e: raise ParsingError(str(e)) from e def reseller_domains(reseller_name=None): """ Get dict[user, domain] Attention!! This function may work unstable. See PTCLLIB-95 for details. :param reseller_name: reseller's name :rtype: dict[str, str|None] :raises DomainException: if cannot obtain domains """ json_string = _reseller_users_json(reseller_name) return _reseller_user_domains_parser(json_string) def get_user_login_url(domain): return f'http://{domain}:2083' def is_no_php_binaries_on_cpanel(): """ Checks that there are no installed php binaries only for cpanel """ try: WhmApiRequest('php_get_installed_versions').call() except WhmNoPhpBinariesError: return True return False class PanelPlugin(GeneralPanelPluginV1): DEFAULT_LOCALE = 'en' BAD_CODING_ERROR_CODE = 48 HTTPD_CONFIG_FILE = '/etc/apache2/conf/httpd.conf' def __init__(self): super().__init__() def invalidate_cpapi_cache(self): """ Goes through all panel caches and invalidates it if needed """ method_marker_pairs = (('_get_php_version_id_to_handler_map', ['/etc/cpanel/ea4/php.conf']), ('_get_vhosts_php_versions', ['/etc/userdatadomains', '/etc/cpanel/ea4/php.conf'])) for pair in method_marker_pairs: method, markers = pair[0], pair[1] cache_file = os.path.join(CPAPI_CACHE_STORAGE, method + '.cache') if self.is_cache_valid(cache_file, markers): # cache is up to dated -> nothing to do continue # cache is outdated -> rewrite data = getattr(self, method)() self.rewrite_cpapi_cache(data, cache_file) def _run_long_script(self, args): """ Processes decoding errors from long script which mean that cpanel wrote something bad to config file (most likely LVEMAN-1150) :param args: arguments to pass :return: stdout, stderr """ with subprocess.Popen( [self._custom_script_name] + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) as p: out, err = p.communicate() returncode = p.returncode if returncode == self.BAD_CODING_ERROR_CODE: raise EncodingError( "Problem with encoding in %(script)s file, error is: '%(error)s'.", script=self._custom_script_name, error=err ) return out, err, returncode def getCPName(self): """ Return panel name :return: """ return __cpname__ def get_cp_description(self): """ Retrieve panel name and it's version :return: dict: { 'name': 'panel_name', 'version': 'panel_version', 'additional_info': 'add_info'} or None if can't get info """ try: with subprocess.Popen( ['/usr/local/cpanel/cpanel', '-V'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) as p: out, _ = p.communicate() return {'name': __cpname__, 'version': out.split()[0], 'additional_info': None} except Exception: return None def db_access(self): """ Getting root access to mysql database. For example {'login': 'root', 'db': 'mysql', 'host': 'localhost', 'pass': '9pJUv38sAqqW'} :return: root access to mysql database :rtype: dict :raises: NoDBAccessData """ return db_access() def cpusers(self): """ Generates a list of cpusers registered in the control panel :return: list of cpusers registered in the control panel :rtype: tuple """ return cpusers() def resellers(self): """ Generates a list of resellers in the control panel :return: list of cpusers registered in the control panel :rtype: tuple """ return resellers() def is_reseller(self, username): """ Check if given user is reseller; :type username: str :rtype: bool """ return is_reseller(username) def dblogin_cplogin_pairs(self, cplogin_lst=None, with_system_users=False): """ Get mapping between system and DB users @param cplogin_lst :list: list with usernames for generate mapping @param with_system_users :bool: add system users to result list or no. default: False """ return dblogin_cplogin_pairs(cplogin_lst, with_system_users) def cpinfo(self, cpuser=None, keyls=('cplogin', 'package', 'mail', 'reseller', 'dns', 'locale'), search_sys_users=True): """ Retrieves info about panel user(s) :param str|unicode|list|tuple|None cpuser: user login :param keyls: list of data which is necessary to obtain the user, the valuescan be: cplogin - name/login user control panel mail - Email users reseller - name reseller/owner users locale - localization of the user account package - User name of the package dns - domain of the user :param bool search_sys_users: search for cpuser in sys_users or in control panel users (e.g. for Plesk) :return: returns a tuple of tuples of data in the same sequence as specified keys in keylst :rtype: tuple """ return cpinfo(cpuser, keyls, search_sys_users=search_sys_users) def get_admin_email(self): """ Retrieve admin email address :return: Host admin's email """ return get_admin_email() @staticmethod def useraliases(cpuser, domain): """ Return aliases from user domain :param str|unicode cpuser: user login :param str|unicode domain: :return list of aliases """ return useraliases(cpuser, domain) def docroot(self, domain): """ Return document root for domain :param str|unicode domain: :return Cortege: (document_root, owner) """ return docroot(domain) def userdomains(self, cpuser): """ Return domain and document root pairs for control panel user first domain is main domain :param str|unicode cpuser: user login :return list of tuples (domain_name, documen_root) """ return userdomains(cpuser) def homedirs(self): """ Detects and returns list of folders contained the home dirs of users of the cPanel :return: list of folders, which are parent of home dirs of users of the panel """ return homedirs() def reseller_users(self, resellername=None): """ Return reseller users :param resellername: reseller name; autodetect name if None :return list[str]: user names list """ return reseller_users(resellername) def reseller_domains(self, resellername=None): """ Get dict[user, domain] Attention!! This function may work unstable. See PTCLLIB-95 for details. :param reseller_name: reseller's name :rtype: dict[str, str|None] :raises DomainException: if cannot obtain domains """ return reseller_domains(resellername) def get_user_login_url(self, domain): """ Get login url for current panel; :type domain: str :rtype: str """ return get_user_login_url(domain) def domain_owner(self, domain): """ Return domain's owner :param domain: Domain/sub-domain/add-domain name :rtype: str :return: user name or None if domain not found """ return domain_owner(domain) def get_system_php_info(self): try: default_version = WhmApiRequest( 'php_get_system_default_version').call()['version'] except WhmApiError as e: raise CPAPIExternalProgramFailed(e) from e return { 'default_version_id': default_version } def get_domains_php_info(self): """ Returns info about domains: username, php_version, handler_type For each domain we detect handler and php_version _get_php_version_id_to_handler_map() returns data of installed versions, so if the version of some domain was removed we can`t detect the handler. In such case we set handler_type to None. Otherwise we detect handler and set it to handler_type :rtype dict """ php_version_to_handler_map = self._get_php_version_id_to_handler_map() php_settings_per_vhost = self._get_vhosts_php_versions() domains_php_info = {} for domain_info in php_settings_per_vhost: php_version_id = domain_info['version'] if php_version_id not in list(php_version_to_handler_map.keys()): logger.error("Unable to find php %s in handlers map %s. ", php_version_id, php_version_to_handler_map, extra={ 'php_version_id': php_version_id, 'php_version_to_handler_map': php_version_to_handler_map }) handler_type = None else: handler_type = 'fpm' if domain_info['php_fpm'] \ else php_version_to_handler_map[php_version_id] domains_php_info[domain_info['vhost']] = DomainDescription( username=domain_info['account'], php_version_id=php_version_id, handler_type=handler_type, display_version=php_version_id ) return domains_php_info @staticmethod def get_installed_php_versions(): """ Get the list of PHP version installed in panel :return: list """ try: # ['alt-php56', 'alt-php72', 'ea-php74'] php_versions = WhmApiRequest('php_get_installed_versions').call()['versions'] except (KeyError, WhmApiError) as e: logger.error('CPAPI: Could not get list of installed PHP versions: %s', e) # todo: consider changing this to return exceptions return [] else: php_description = [] for php_name in php_versions: if php_name.startswith('alt-'): php_root_dir = f'/opt/{php_name.replace("-", "/")}/' php_description.append(PHPDescription( identifier=php_name, version=f'{php_name[-2]}.{php_name[-1]}', dir=os.path.join(php_root_dir), modules_dir=os.path.join(php_root_dir, 'usr/lib64/php/modules/'), bin=os.path.join(php_root_dir, 'usr/bin/php'), ini=os.path.join(php_root_dir, 'link/conf/default.ini'), )) elif php_name.startswith('ea-'): php_root_dir = f'/opt/cpanel/{php_name}/root/' php_description.append(PHPDescription( identifier=php_name, version=f'{php_name[-2]}.{php_name[-1]}', modules_dir=os.path.join(php_root_dir, 'usr/lib64/php/modules/'), dir=os.path.join(php_root_dir), bin=os.path.join(php_root_dir, 'usr/bin/php'), ini=os.path.join(php_root_dir, 'etc/php.ini'), )) else: # unknown php, skip continue return php_description @staticmethod @GeneralPanelPluginV1.cache_call(panel_parker=['/etc/userdatadomains', '/etc/cpanel/ea4/php.conf']) def _get_vhosts_php_versions(): """ See https://documentation.cpanel.net/display/DD/WHM+API+1+Functions+-+php_get_vhost_versions :rtype: dict """ try: return WhmApiRequest('php_get_vhost_versions').call()['versions'] except WhmApiError as e: raise CPAPIExternalProgramFailed(e) from e @staticmethod @GeneralPanelPluginV1.cache_call(panel_parker=['/etc/cpanel/ea4/php.conf']) def _get_php_version_id_to_handler_map(): """ Returns dict with info about php version and it`s current handler: {'ea-php56': 'cgi', 'ea-php72': 'suphp', 'alt-php51': 'suphp', 'alt-php52': 'suphp' ...} Using cpanel whmapi request Tries to get all handlers or if there is problem with some handler - gets handlers one by one As a result information could be incomplete if some handlers are not available See https://documentation.cpanel.net/display/DD/WHM+API+1+Functions+-+php_get_handlers :rtype: dict """ try: handlers = WhmApiRequest('php_get_handlers').call()['version_handlers'] except WhmApiError as e: logger.error("Unable to get information about php handlers, " "falling back to per-handler data gathering. " "Error happened: %s", e, extra={ 'error_message': e.message, 'error_context': e.context }) handlers = PanelPlugin._get_handler_info_for_each_version() return { php['version']: php['current_handler'] for php in handlers } @staticmethod def _get_handler_info_for_each_version(): """ Gets handler data from each version one by one, so that data can still be collected even when one of the installed versions is broken. :rtype: list """ handlers = [] installed_php_versions = PanelPlugin.get_installed_php_versions() for version in installed_php_versions: # {'version_handlers': [{'available_handlers': ['cgi', 'none'], 'version': 'ea-php72', # 'current_handler': None}]} try: version_handler = \ WhmApiRequest('php_get_handlers').with_arguments( version=version['identifier'] ).call()['version_handlers'][0] handlers.append(version_handler) except (KeyError, WhmApiError) as e: logger.error('CPAPI: Could not get data for PHP version: %s', e) continue return handlers def get_admin_locale(self): cpanel_config = load_fast(CPANEL_CONFIG) try: server_locale = cpanel_config['server_locale'] if server_locale: return server_locale return PanelPlugin.DEFAULT_LOCALE except KeyError: return PanelPlugin.DEFAULT_LOCALE @staticmethod def get_apache_connections_number(): """ Retrieves Apache's connections number :return: tuple (conn_num, message) conn_num - current connections number, 0 if error message - OK/Trace """ # curl http://127.0.0.1/whm-server-status?auto | grep "Total Accesses" try: url = 'http://127.0.0.1/whm-server-status?auto' response = requests.get(url, timeout=5) if response.status_code != 200: return 0, f"GET {url} response code is {response.status_code}" s_response = response.content.decode('utf-8') s_response_list = s_response.split('\n') out_list = list(grep("Total Accesses", data_from_file=s_response_list)) # out_list example: ['Total Accesses: 200'] s_total_accesses = out_list[0].split(':')[1].strip() return int(s_total_accesses), 'OK' except Exception: return 0, format_exc() @staticmethod def get_apache_ports_list(): """ Retrieves active httpd's ports from httpd's config :return: list of apache's ports """ # cat /etc/apache2/conf/httpd.conf | grep Listen _httpd_ports_list = [] try: lines = get_file_lines(PanelPlugin.HTTPD_CONFIG_FILE, unicode_errors_handle='surrogateescape') except (OSError, IOError): return None lines = [line.strip() for line in lines] for line in grep('Listen', match_any_position=False, multiple_search=True, data_from_file=lines): # line examples: # Listen 0.0.0.0:80 # Listen [::]:80 try: value = int(line.split(':')[-1]) if value not in _httpd_ports_list: _httpd_ports_list.append(value) except (IndexError, ValueError): pass if not _httpd_ports_list: _httpd_ports_list.append(80) return _httpd_ports_list @staticmethod def get_apache_max_request_workers(): """ Get current maximum request apache workers from httpd's config :return: tuple (max_req_num, message) max_req_num - Maximum request apache workers number or 0 if error message - OK/Trace """ # cat /etc/apache2/conf/httpd.conf | grep MaxRequestWorkers # MaxRequestWorkers 150 try: lines = get_file_lines(PanelPlugin.HTTPD_CONFIG_FILE, unicode_errors_handle='surrogateescape') mrw_list = list(grep('MaxRequestWorkers', match_any_position=False, data_from_file=lines)) if len(mrw_list) != 1: return 0, 'MaxRequestWorkers directive is absent or multiple in httpd\'s config' parts = mrw_list[0].split(' ') if len(parts) == 2: return int(parts[1]), 'OK' return 0, f'httpd config line syntax error. Line is \'{mrw_list[0]}\'' except (OSError, IOError, IndexError, ValueError): return 0, format_exc() @staticmethod def get_user_emails_list(username: str, domain: str) -> str: # "acct" : [ # { # "has_backup" : 0, # "email" : "bla@cloudlinux.com, blabla@gmail.com" # } # ] emails = \ WhmApiRequest('listaccts').with_arguments(want='email', searchmethod='exact', search=username, searchtype='user').call()['acct'][0] user_emails = emails['email'] if user_emails == '*unknown*': return '' return user_emails @staticmethod def panel_login_link(username): link = WhmApiRequest('create_user_session').with_arguments(user=username, service='cpaneld').call()['url'] if not link: return '' # https://77.79.198.14:2083/cpsess3532861743/login/?session=stackoverflow%3ascBEPeVeSXqZgMLs%.. -> # https://77.79.198.14:2083/ parsed = urlparse(link) return f'{parsed.scheme}://{parsed.netloc}/' @staticmethod def panel_awp_link(username): link = PanelPlugin.panel_login_link(username).rstrip("/") if len(link) == 0: return '' return f'{link}/cpsess0000000000/frontend/paper_lantern/lveversion/wpos.live.pl' def get_server_ip(self): try: with open('/var/cpanel/mainip', encoding='utf-8') as f: return f.read().strip() except FileNotFoundError as e: raise NotSupported( 'Unable to detect main ip for this server. ' 'Contact CloudLinux support and report the issue.' ) from e def suspended_users_list(self): # [{'time': 'Tue Mar 26 12:41:31 2024', 'owner': 'root', 'is_locked': 0, # 'unixtime': 1711456891, 'reason': 'Unknown', 'user': 'susp2'}, # {'is_locked': 0, 'time': 'Tue Mar 26 12:18:53 2024', 'owner': # 'root', 'reason': 'Unknown', 'user': 'susp', 'unixtime': 1711455533}] suspended_info = WhmApiRequest('listsuspended').call()['account'] return [item['user'] for item in suspended_info] def get_unsupported_cl_features(self): return tuple()