ok

Mini Shell

Direktori : /opt/cloudlinux/venv/lib/python3.11/site-packages/clwpos/object_cache/
Upload File :
Current File : //opt/cloudlinux/venv/lib/python3.11/site-packages/clwpos/object_cache/redis_utils.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

from __future__ import absolute_import

import os
import re
import subprocess
from functools import lru_cache
from pathlib import Path
from typing import List
from pkg_resources import parse_version

from secureio import write_file_via_tempfile

from clcommon.cpapi import getCPName, CPANEL_NAME, PLESK_NAME, DIRECTADMIN_NAME

from clwpos.constants import (
    RedisRequiredConstants,
    EA_PHP_PREFIX,
    PLESK_PHP_PREFIX,
    DIRECTADMIN_PREFIX,
    CAGEFSCTL
)
from clwpos.data_collector_utils import get_cached_php_installed_versions
from clwpos.php.base import PHP
from clwpos.logsetup import setup_logging

from clwpos.utils import (
    daemon_communicate,
    run_in_cagefs_if_needed,
    create_pid_file,
    acquire_lock
)

_logger = setup_logging(__name__)

BASE_CPANEL_EA_PHP_DIR = '/opt/cpanel'
BASE_PLESK_PHP_DIR = '/opt/plesk/php'


def configurator():
    """Instantiate appropriate configurator"""

    panel = getCPName()
    if panel == CPANEL_NAME:
        return EaPhpRedisConfigurator()
    elif panel == PLESK_NAME:
        return PleskPhpRedisConfigurator()
    elif panel == DIRECTADMIN_NAME:
        return DirectAdminPhpRedisConfigurator()

    raise Exception("No PHP Redis configurator currently found")


class RedisConfigurator:

    def configure(self):
        with acquire_lock(os.path.join('/var/run', self.PHP_PREFIX),
                          attempts=1):
            self.configure_redis_extension()

    def _update_cagefs(self, need_cagefs_update, wait_child_process):
        if need_cagefs_update and wait_child_process and os.path.isfile(
                CAGEFSCTL):
            try:
                subprocess.run([CAGEFSCTL, '--check-cagefs-initialized'],
                               stdout=subprocess.DEVNULL,
                               stderr=subprocess.DEVNULL,
                               check=True)
            except subprocess.CalledProcessError:
                _logger.info(
                    'CageFS in uninitialized, skipping force-update')
            else:
                subprocess.run(
                    [CAGEFSCTL, '--wait-lock', '--force-update'],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL)

    def configure_redis_extension(self):
        """
        Sets up redis if needed:
         - installing package
         - enables in .ini file
        """
        need_cagefs_update = False
        wait_child_process = bool(os.environ.get('CL_WPOS_WAIT_CHILD_PROCESS'))
        php_versions_redis_data = {
            php: _redis_extension_info(php) for php in
            self.get_supported_php()
        }

        php_versions_to_enable_redis = []
        for php, redis_data in php_versions_redis_data.items():
            if redis_data.get('is_present') and redis_data.get('is_loaded'):
                _logger.info('Redis extension is already installed and configured for %s', php.identifier)
                continue
            php_versions_to_enable_redis.append(php)

        if not php_versions_to_enable_redis:
            _logger.info('All ea-php versions have redis installed and active')
            return

        with create_pid_file(self.PHP_PREFIX):
            for php in php_versions_to_enable_redis:
                redis_data = php_versions_redis_data.get(php)
                if not redis_data.get('is_present'):
                    redis_package = self.redis_package(php)
                    _logger.info('Trying to install %s package', redis_package)
                    result = subprocess.run(
                        ['yum', '-y', 'install', *self._additional_repos,
                         redis_package],
                        capture_output=True,
                        text=True)
                    if result.returncode != 0 and 'Nothing to do' not in result.stdout:
                        _logger.error(
                            'Failed to install package %s, due to reason: %s',
                            redis_package,
                            f'{result.stdout}\n{result.stderr}')
                        continue
                    _logger.info('Package successfully installed, activating it')
                    self.enable_redis_extension(php)
                    need_cagefs_update = True
                elif not redis_data.get('is_loaded'):
                    self.enable_redis_extension(php)
                    need_cagefs_update = True

            self._update_cagefs(need_cagefs_update, wait_child_process)

    def enable_redis_extension(self, php_version):
        """
        Enables (if needed) redis extension in .ini config
        """
        path = self.redis_ini(php_version)
        keyword = 'redis.so'
        if not os.path.exists(path):
            _logger.error(
                'Redis extension config: %s is not found, ensure corresponding rpm package installed: %s',
                str(path), self.redis_package(php_version))
            return
        with open(path) as f:
            extension_data = f.readlines()

        uncommented_pattern = re.compile(fr'^\s*extension\s*=\s*{keyword}')
        commented_pattern = re.compile(fr'^\s*;\s*extension\s*=\s*{keyword}')
        enabled_line = f'extension = {keyword}\n'
        was_enabled = False
        lines = []

        for line in extension_data:
            if uncommented_pattern.match(line):
                return
            if not was_enabled and commented_pattern.match(line):
                lines.append(enabled_line)
                was_enabled = True
            else:
                lines.append(line)
        if not was_enabled:
            lines.append(enabled_line)
        write_file_via_tempfile(''.join(lines), path, 0o644)

    @property
    def _additional_repos(self):
        return tuple()

    @property
    def PHP_PREFIX(self):
        raise NotImplementedError

    def get_supported_php(self) -> List[PHP]:
        """"""
        raise NotImplementedError

    def redis_package(self, php: PHP) -> str:
        raise NotImplementedError

    def redis_ini(self, php_version: PHP) -> Path:
        raise NotImplementedError


class EaPhpRedisConfigurator(RedisConfigurator):
    """
    Install and configure redis extensions for cPanel ea-php
    """

    @property
    def PHP_PREFIX(self):
        return EA_PHP_PREFIX

    def get_supported_php(self) -> List[PHP]:
        """
        Looks through /opt/cpanel and gets installed phps
        """
        php_versions = get_cached_php_installed_versions()
        minimal_supported = parse_version('74')

        supported = []
        for php_description in php_versions:
            if php_description.identifier.startswith('ea-php') \
                    and os.path.exists(php_description.bin) \
                    and parse_version(php_description.identifier.replace('ea-php', '')) >= minimal_supported:
                supported.append(php_description)

        return supported

    def redis_package(self, php):
        return f'{php.identifier}-php-redis'

    def redis_ini(self, php_version: PHP) -> Path:
        return Path(php_version.dir).joinpath('etc/php.d/50-redis.ini')

class DirectAdminPhpRedisConfigurator(RedisConfigurator):
    """
    Installs and configure redis extensions for DirectAdmin php

    NOTE: directadmin enables redis for all compiled versions or for none

    https://docs.directadmin.com/webservices/php/php-extensions.html#installing-extensions
    """
    @property
    def PHP_PREFIX(self):
        return DIRECTADMIN_PREFIX

    def is_redis_already_enabled(self):
        """
        If at least for 1 supported version redis is not loaded -> False
        """
        supported_versions = self.get_supported_php()
        for version_item in supported_versions:
            if not is_php_extension_loaded(version_item, 'redis', is_present=True):
                return False
        return True

    def configure_redis_extension(self):
        wait_child_process = bool(os.environ.get('CL_WPOS_WAIT_CHILD_PROCESS'))
        with create_pid_file(self.PHP_PREFIX):
            try:
                if not self.is_redis_already_enabled():
                    subprocess.run(['/usr/local/directadmin/custombuild/build', 'set_php', 'redis', 'yes'],
                                   capture_output=True,
                                   text=True)
                    subprocess.run(['/usr/local/directadmin/custombuild/build', 'php_redis'],
                                   capture_output=True,
                                   text=True)
                    self._update_cagefs(need_cagefs_update=True, wait_child_process=wait_child_process)
            except Exception:
                _logger.exception('Error on configuring redis extension for DirectAdmin')

    def get_supported_php(self) -> List[PHP]:
        php_versions = get_cached_php_installed_versions()
        minimal_supported = parse_version('74')

        supported = []
        for php_description in php_versions:
            if (php_description.identifier.startswith(DIRECTADMIN_PREFIX) and
                    parse_version(php_description.version.replace('.', '')) >= minimal_supported):
                supported.append(php_description)
        return supported


class PleskPhpRedisConfigurator(RedisConfigurator):
    """
    Install and configure redis extensions for Plesk php
    """

    @property
    def _additional_repos(self):
        return '--enablerepo', 'PLESK*'

    @property
    def PHP_PREFIX(self):
        return PLESK_PHP_PREFIX

    def get_supported_php(self) -> List[PHP]:
        """
        Looks through /opt/plesk/php and gets installed phps.
        /opt/plesk/php contains plain version directories, e.g. 7.4; 8.0; 8.1
        """
        php_versions = get_cached_php_installed_versions()
        minimal_supported = parse_version('74')

        supported = []
        for php_description in php_versions:
            if php_description.identifier.startswith('plesk-php') \
                    and os.path.exists(php_description.bin) \
                    and parse_version(php_description.identifier.replace('plesk-php', '')) >= minimal_supported:
                supported.append(php_description)

        return supported

    def redis_package(self, php):
        return f'{php.identifier}-redis'

    def redis_ini(self, php_version):
        return Path(php_version.dir).joinpath(f'etc/php.d/redis.ini')


@lru_cache()
def _redis_extension_info(version: PHP) -> dict:
    is_present = bool(list(Path(version.modules_dir).glob("**/redis.so")))
    is_loaded = is_php_extension_loaded(version, 'redis', is_present)

    return {
        "is_present": is_present,
        "is_loaded": is_loaded
    }

def is_php_extension_loaded(version: PHP, extension: str, is_present: bool):
    php_bin_path = version.bin
    if os.geteuid() == 0:
        exec_func = subprocess.run
    else:
        exec_func = run_in_cagefs_if_needed

    is_loaded = exec_func(
        f'{php_bin_path} -m | /bin/grep {extension}',
        shell=True,
        executable='/bin/bash',
        env={}
    ).returncode == 0 if is_present else False
    return is_loaded


def filter_php_versions_with_not_loaded_redis(php_versions: List[PHP]) -> List[PHP]:
    """
    Filter list of given php versions to find out
    for which redis extension is presented but not loaded.
    """
    php_versions_with_not_loaded_redis = []
    for version in php_versions:
        php_redis_info = _redis_extension_info(version)
        if not php_redis_info['is_loaded'] and php_redis_info['is_present']:
            php_versions_with_not_loaded_redis.append(version)
    return php_versions_with_not_loaded_redis


@lru_cache(maxsize=None)
def get_cached_php_versions_with_redis_loaded() -> set:
    """
    List all installed php version on the system which has redis-extension enabled
    :return: installed php versions which has redis-extension
    """
    versions = get_cached_php_installed_versions()
    return {version for version in versions if _redis_extension_info(version)["is_loaded"]}


@lru_cache(maxsize=None)
def get_cached_php_versions_with_redis_present() -> set:
    """
    List all installed php version on the system which has redis-extension installed
    :return: installed php versions which has redis-extension installed
    """
    versions = get_cached_php_installed_versions()
    return {version for version in versions if _redis_extension_info(version)["is_present"]}


def reload_redis(uid: int = None, force: str = 'no'):
    """
    Make redis reload via CLWPOS daemon
    :param uid: User uid (optional)
    :param force: force reload w/o config check
    """
    cmd_dict = {"command": "reload", 'force_reload': force}
    if uid:
        cmd_dict['uid'] = uid
    daemon_communicate(cmd_dict)

Zerion Mini Shell 1.0