HOME


Mini Shell 1.0
DIR: /opt/cloudlinux/venv/lib64/python3.11/site-packages/xray/manager/
Upload File :
Current File : //opt/cloudlinux/venv/lib64/python3.11/site-packages/xray/manager/cpanel.py
# -*- 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'