/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
clwpos
/
object_cache
/
Upload Filee
HOME
# 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/LICENCE.TXT # # Redis manipulation library for reloader script # pylint: disable=no-absolute-import import pwd import os import re import traceback import subprocess from logging import Logger from typing import List, Optional from secureio import write_file_via_tempfile from clcommon.clpwd import drop_user_privileges from clcommon.cpapi import userdomains from clcommon.const import Feature from clcommon.cpapi import is_panel_feature_supported from clcommon.utils import demote from clwpos import gettext as _ from clwpos.cl_wpos_exceptions import WposError from clwpos.daemon_redis_lib import _get_redis_pid_from_pid_file_with_wait, kill_process_by_pid from clwpos.utils import USER_WPOS_DIR, is_run_under_user from clwpos.user.config import UserConfig, ConfigError from clwpos.optimization_features import OBJECT_CACHE_FEATURE from clwpos.feature_suites import get_allowed_modules from clwpos.constants import REDIS_SERVER_BIN_FILE from clwpos.logsetup import NullLogger from clcommon.cpapi.cpapiexceptions import NoPackage _USER_REDIS_CONF_PATTERN = """# !!! WARNING !!! AUTO-GENERATED FILE, PLEASE DO NOT MODIFY IT maxclients {maxclients} databases 16 maxmemory-policy allkeys-lru appendonly no appendfsync always # Disable TCP ports using port 0 unixsocket {socket_path} unixsocketperm 600 dir {clwpos_dir} maxmemory {maxmemory} save "" pidfile {pidfile} """ logger = NullLogger() def _read_maxclients_from_config(redis_config_path: str) -> str: """ Read and return maxclients value from the Redis config file if config file exists maxclients is integer value. Return default value otherwise. :param redis_config_path: path to Redis configuration file :return: maxclients value from config or default """ if os.path.exists(redis_config_path): with open(redis_config_path) as redis_config: config = redis_config.read() value = re.search(r'maxclients (\d+)$', config, flags=re.MULTILINE) if value is not None: return value.group(1) return '16' def _write_redis_config_for_user(username: str, user_homedir: str, clwpos_dir: str, redis_socket_path: str, redis_config_path: str, maxmemory: str, pidfile_path: str): """ Writes redis config for user (call under drop_privileges) :param username: User name for write config :param user_homedir: User's homedir :param redis_socket_path: Full path to user's redis socket :param redis_config_path: Full path to user's redis config file :param maxmemory: maxmemory value to write to redis config :param pidfile_path: Redis pid file path :return: None """ maxclients = _read_maxclients_from_config(redis_config_path) redis_config_content = _USER_REDIS_CONF_PATTERN.format( maxclients=maxclients, socket_path=redis_socket_path, clwpos_dir=clwpos_dir, maxmemory=maxmemory, pidfile=pidfile_path ) try: clwpos_dir = os.path.join(user_homedir, USER_WPOS_DIR) try: os.makedirs(clwpos_dir, exist_ok=True) except (OSError, IOError,) as system_error: raise WposError(message=_("Can't create directory %(directory)s. "), details=_("The operating system reported error: %(system_error)s"), context={"directory": clwpos_dir, 'system_error': system_error}) # Write redis config write_file_via_tempfile(redis_config_content, redis_config_path, 0o600) except (OSError, IOError) as system_error: raise WposError(message=_("Error happened while writing redis config for user '%(user)s' "), details=_("The operating system reported error: %(system_error)s"), context={"user": username, "system_error": system_error}) def _get_valid_docroots_from_config(logger: Logger, username: str, user_homedir: str, docroot_list_to_validate: List[str]) -> List[str]: """ Validates docroots from list :param logger: Logger to log errors :param username: User name :param user_homedir: User's homedir :param docroot_list_to_validate: Docroot list to validate :return: List of valid docroots """ if not docroot_list_to_validate: # Nothing to validate return [] try: # Get active docroots for user from panel userdomains_data_list = userdomains(username) except (OSError, IOError, IndexError, NoPackage) as e: # Skip all cpapi.userdomains errors logger.warning("Can't get user list from panel: %s", str(e)) return [] # Add some filename to all docroots to avoid any problems with / at the end of paths docroots_from_panel = [] for _, document_root in userdomains_data_list: docroots_from_panel.append(os.path.normpath(document_root)) # Validate docroots from config valid_docroot_list: List[str] = [] for _dr in docroot_list_to_validate: tempname = os.path.normpath(os.path.join(user_homedir, _dr)) if tempname in docroots_from_panel: valid_docroot_list.append(_dr) return valid_docroot_list def _is_redis_plugin_enabled(logger: Logger, username: str, uid: int, user_homedir: str, user_config_dict: dict) -> bool: """ Get redis status according to user's and admin's configs :param logger: Logger to log errors :param username: User name :parem uid: User uid :param user_homedir: User's homedir :param user_config_dict: User's config :return: True - module enabled, False - disabled """ # TODO: Refactor this function in LU-2613 # # Admin's config example: # allowed_modules_list example: ['object_cache'] allowed_modules_list = get_allowed_modules(uid) if OBJECT_CACHE_FEATURE not in allowed_modules_list: return False # WPOS enabled by admin, check user's config # User's config example: # { # "docroots": { # "public_html": {"": ["object_cache"], "1": []}, # "public_html/gggh.com": {"wp1": ["object_cache"], "wp2": []} # }, # "maxmemory": "512mb" # } target_docroots = user_config_dict.get('docroots', {}) docroots_to_validate: list = list(target_docroots.keys()) valid_wp_docroots = _get_valid_docroots_from_config(logger, username, user_homedir, docroots_to_validate) for wp_docroot in valid_wp_docroots: wp_data = target_docroots.get(wp_docroot, {}) for module_list in wp_data.values(): # By WP paths if isinstance(module_list, list) and OBJECT_CACHE_FEATURE in module_list: # Redis enabled return True return False def __start_redis(is_cagefs_missing: bool, redis_config_path: str, username) -> None: """ Run subprocess with redis """ user_pwd = pwd.getpwnam(username) if is_cagefs_missing: # CL Solo subprocess.Popen( [REDIS_SERVER_BIN_FILE, redis_config_path], preexec_fn=demote(user_pwd.pw_uid, user_pwd.pw_gid), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) else: # CL Shared # cagefs_enter.proxied brought by lve-wrappers which is required by lvemanager/lve-utils subprocess.Popen( ['/bin/cagefs_enter.proxied', REDIS_SERVER_BIN_FILE, redis_config_path], preexec_fn=demote(user_pwd.pw_uid, user_pwd.pw_gid), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) pidfile_path = os.path.join(user_pwd.pw_dir, USER_WPOS_DIR, 'redis.pid') redis_pid = _get_redis_pid_from_pid_file_with_wait(pidfile_path) if redis_pid is None: raise OSError('Unable to detect redis pid. Try again later.') return None def _start_redis_server_for_user(_logger: Logger, username: str, maxmemory: str) -> dict: """ Start redis server for supplied user (calls under user) :param _logger: Daemon's logger :param username: Username to setup redis :param maxmemory: maxmemory value to write to redis config :return: dict If redis was started for user - {"result": "success"} else - redis was not started - {"result": "error", "context": "...."} """ user_pwd = pwd.getpwnam(username) # /home/username/.clwpos/ _logger.info('Starting redis for username=%s', username) wpos_dir = os.path.join(user_pwd.pw_dir, USER_WPOS_DIR) redis_socket_path = os.path.join(wpos_dir, 'redis.sock') redis_config_path = os.path.join(wpos_dir, 'redis.conf') pidfile_path = os.path.join(wpos_dir, 'redis.pid') _write_redis_config_for_user(username, user_pwd.pw_dir, wpos_dir, redis_socket_path, redis_config_path, maxmemory, pidfile_path) try: is_cagefs_available = is_panel_feature_supported(Feature.CAGEFS) __start_redis(not is_cagefs_available, redis_config_path, username) return {"result": "success", "redis_enabled": True} except (OSError, IOError) as e: str_exc = traceback.format_exc() _logger.warning("Error while starting redis server for user %s. Error is: %s", username, str_exc) return {"result": _("Error while starting redis server: %(error)s"), "context": {"error": str(e)}} def _check_redis_process_and_kill(old_redis_pid: int) -> bool: """ Check existing process by PID and kill it: - If old_redis_pid == -1, nothing will kill, return False - else check process existance. if exi :param old_redis_pid: PID to check and kill :return: True - Can't kill process not owned by current user, this is error, continue work not allowed False - Process absent or was killed succesfully, continue work allowed """ if old_redis_pid == -1: # Process absent or was killed succesfully, continue work allowed return False if not is_run_under_user(): raise WposError("Internal error! Trying to kill process with root privileges") try: os.kill(old_redis_pid, 0) except PermissionError: # process not owned by current user -- error, exit return True except ProcessLookupError: # No such process - no error pass kill_process_by_pid(logger, old_redis_pid) # Process absent or was killed succesfully, continue work allowed return False def reload_redis_for_user(username: str, old_redis_pid: int, _logger: Optional[Logger] = None, is_log_debug_level: bool = False, force_reload: str = 'no') -> dict: """ Reloads redis for supplied user without logging and killing old redis process Calls only from redis_reloader.py script :param username: Username to setup redis :param old_redis_pid: Redis PID to kill, -1 - no PID to kill :param _logger: Daemon's logger :param is_log_debug_level: True - log messages/ False - no :param force_reload: reload redis w/o conditions :return: dict If redis was started for user - {"result": "success"} else - redis was not started - {"result": "error", "context": "...."} """ if _logger is None: _logger = logger _logger.info('[Reload redis for user]: Request to reload redis for ' 'username=%s, ' 'old redis pid=%s,' 'force_reload', username, str(old_redis_pid), str(force_reload)) drop_user_privileges(username, effective_or_real=False, set_env=False) if _check_redis_process_and_kill(old_redis_pid): return {"result": _("Can't kill old redis process for user '%(user)s'. PID is %(pid)s"), "context": {"user": username, "pid": old_redis_pid}} try: user_config_class = UserConfig(username) user_config = user_config_class.read_config() except ConfigError as e: # User's config read error if is_log_debug_level: _logger.warning("Can't reload redis for user %s. User's config read error: %s", username, str(e)) if not force_reload: return {"result": _("Can't reload redis for user '%(user)s'. User's config read error"), "context": {"user": username}} else: _logger.info('Cannot read user config, but force_reload was passed ---> unconditional reload') user_config = {} pw_user = pwd.getpwnam(username) is_redis_enable = _is_redis_plugin_enabled(_logger, username, pw_user.pw_uid, pw_user.pw_dir, user_config) if is_redis_enable or force_reload == 'yes': _logger.info('Starting redis for username=%s', username) # Start redis return _start_redis_server_for_user(_logger, username, user_config.get('max_cache_memory', UserConfig.DEFAULT_MAX_CACHE_MEMORY)) # Redis is disabled by user's config return {"result": "success", "redis_enabled": False}