%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /opt/cloudlinux/venv/lib64/python3.11/site-packages/clwpos/object_cache/
Upload File :
Create Path :
Current File : //opt/cloudlinux/venv/lib64/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 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_to_enable_redis = []
        for php in self.get_supported_php():
            if php.is_extension_loaded('redis'):
                _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:
                if not php.is_extension_installed('redis'):
                    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 php.is_extension_loaded('redis'):
                    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 version_item.is_extension_loaded('redis'):
                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')


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:
        if not version.is_extension_loaded('redis') and version.is_extension_installed('redis'):
            php_versions_with_not_loaded_redis.append(version)
    return php_versions_with_not_loaded_redis


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

Zerion Mini Shell 1.0