ok
Direktori : /opt/cloudlinux/venv/lib/python3.11/site-packages/clwpos/object_cache/ |
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)