# -*- coding: utf-8 -*-
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
"""
This module contains classes implementing X-Ray Manager behaviour
for cPanel
"""
import json
import subprocess
from collections import ChainMap
from pipes import quote
from clcommon.utils import get_cl_version
from clcommon.utils import is_litespeed_running
from .base import BaseManager
from ..internal.exceptions import XRayManagerError
from ..internal.types import DomainInfo
from ..internal.user_plugin_utils import (
user_mode_verification,
with_fpm_reload_restricted
)
class CPanelManager(BaseManager):
"""
Class implementing an X-Ray manager behaviour for cPanel
"""
VERSIONS_cPanel = {
'ea-php54': '/opt/cpanel/ea-php54/root/etc/php.d',
'ea-php55': '/opt/cpanel/ea-php55/root/etc/php.d',
'ea-php56': '/opt/cpanel/ea-php56/root/etc/php.d',
'ea-php70': '/opt/cpanel/ea-php70/root/etc/php.d',
'ea-php71': '/opt/cpanel/ea-php71/root/etc/php.d',
'ea-php72': '/opt/cpanel/ea-php72/root/etc/php.d',
'ea-php73': '/opt/cpanel/ea-php73/root/etc/php.d',
'ea-php74': '/opt/cpanel/ea-php74/root/etc/php.d'
}
try:
if get_cl_version() != 'cl6':
VERSIONS_cPanel['ea-php80'] = '/opt/cpanel/ea-php80/root/etc/php.d'
VERSIONS_cPanel['ea-php81'] = '/opt/cpanel/ea-php81/root/etc/php.d'
VERSIONS_cPanel['ea-php82'] = '/opt/cpanel/ea-php82/root/etc/php.d'
except TypeError:
# primarily for the test run on the build system
pass
def supported_versions(self) -> ChainMap:
"""
Get supported PHP versions
:return: a chained map with basic supported versions
and cPanel supported versions
"""
return ChainMap(self.VERSIONS,
self.VERSIONS_cPanel)
@staticmethod
def generate_args_list(cmd: str, **kwargs) -> list:
"""
Generate WHMAPI command arguments list from given parameters
:param cmd: api command itself
:param kwargs: additional arguments
:return: list
"""
main_api = ['/usr/sbin/whmapi1', cmd]
additional = [quote(f'{k}={v}') for k, v in kwargs.items()]
output_format = ['--output', 'json']
return main_api + additional + output_format
def whmapi_command(self, cmd: str, **kwargs) -> dict:
"""
Run whmapi1 command and load json result
:param cmd: valid whmapi command
:kwargs: keyword arguments
:return: loaded result, e.g. dict
"""
args = self.generate_args_list(cmd, **kwargs)
result = subprocess.run(args, capture_output=True, text=True)
try:
loaded_result = json.loads(result.stdout)
except json.decoder.JSONDecodeError as e:
self.logger.error(
'Failed to parse JSON from a WHMAPI command output',
extra={'command': cmd,
'result_json': result.stdout})
raise XRayManagerError(
f"Failed to parse JSON from a WHMAPI '{cmd}' command output: {e}", needs_logging=False) from e
try:
if not loaded_result['metadata']['result']:
self.logger.error('WHMAPI failed',
extra={'json': loaded_result})
raise XRayManagerError(loaded_result['metadata']['reason'], needs_logging=False)
except KeyError as e:
self.logger.error(
'Failed to retrieve WHMAPI info: no %s field found', e,
extra={'json': json.loads(result.stdout)})
raise XRayManagerError(
f"Failed to retrieve WHMAPI info: no {e} field found", needs_logging=False) from e
return loaded_result
def resolve_alias(self, domain_name: str) -> str:
"""
Try to resolve domain_name if it is an alias
:param domain_name: original domain name
:return: resolved domain name alias
"""
try:
result = self.whmapi_command('domainuserdata', domain=domain_name)
except XRayManagerError as e:
if 'system does not have a domain named' in str(e):
self.logger.warning('Domain does not exist on the server',
extra={'domain_name': domain_name})
raise XRayManagerError(
f"Domain '{domain_name}' does not exist on this server",
errno=1110, needs_logging=False) from e
else:
try:
return result['data']['userdata']['servername']
except KeyError as e:
self.logger.error(
'Failed to resolve alias: no %s field found', e,
extra={'json': result})
raise XRayManagerError(
f"Failed to resolve alias: no {e} field found", needs_logging=False) from e
def check_domain(self, name: str, domains_data: list,
original_name=None) -> DomainInfo:
"""
Try to find given name among known domains
:param name: name of domain to find
:param domains_data: list of known domains
:param original_name: original domain name (in case of alias resolving)
:return: a DomainInfo object
"""
for domain in (item for item in domains_data if
item['vhost'] == name):
self.logger.info(
'Retrieved domain info: domain %s owned by %s uses php version %s',
name, domain['account'], domain['version'])
return DomainInfo(name=original_name if original_name else name,
panel_php_version=domain['version'],
user=domain['account'],
panel_fpm=domain[
'php_fpm'] if not is_litespeed_running() else False)
self.logger.warning('Domain does not exist on the server',
extra={'domain_name': name})
raise XRayManagerError(
f"Domain '{name}' does not exist on this server", errno=1110, needs_logging=False)
@user_mode_verification
@with_fpm_reload_restricted
def get_domain_info(self, domain_name: str) -> DomainInfo:
"""
Retrieve information about given domain from control panel environment:
PHP version, user of domain.
Try to resolve alias if domain was not found in API response
:param domain_name: name of domain
:return: a DomainInfo object
"""
result = self.whmapi_command('php_get_vhost_versions')
try:
domain_php = result['data']['versions']
except KeyError as e:
self.logger.error(
'Failed to retrieve domain info: no %s field found', e,
extra={'json': result})
raise XRayManagerError(
f"Failed to retrieve domain info: no {e} field found", needs_logging=False) from e
try:
_info = self.check_domain(domain_name, domain_php)
except XRayManagerError:
alias = self.resolve_alias(domain_name)
_info = self.check_domain(alias, domain_php,
original_name=domain_name)
return _info
def domain_default_version_allows_selector(self, domain_php_version: str) -> bool:
"""
Check if given domain uses system default version.
And system default is not alt-php.
If yes, then it means that selector could be applied for given domain
:param domain_php_version: PHP version of domain
:return: True if yes, False otherwise
"""
result = self.whmapi_command('php_get_system_default_version')
try:
default_php = result['data']['version']
except KeyError as e:
self.logger.error(
'Failed to retrieve system default PHP version: no %s field found',
e, extra={'json': result})
raise XRayManagerError(
f"Failed to retrieve system default PHP version: no {e} field found", needs_logging=False) from e
return 'alt-php' not in default_php and default_php == domain_php_version
def panel_specific_selector_enabled(self, domain_info: DomainInfo) -> bool:
"""
Check if selector is enabled specifically for panel
Required to be implemented by child classes
:param domain_info: a DomainInfo object
:return: True if yes, False otherwise
"""
return self.domain_default_version_allows_selector(
domain_info.panel_php_version) and not domain_info.panel_fpm
def fpm_service_name(self, dom_info: DomainInfo) -> str:
"""
Get cPanel FPM service name
:param dom_info: a DomainInfo object
:return: FPM service name
"""
return f'{dom_info.panel_php_version}-php-fpm'
|