Current File : //opt/alt/python37/lib/python3.7/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
from clwpos.constants import (
RedisRequiredConstants,
EA_PHP_PREFIX,
PLESK_PHP_PREFIX,
CAGEFSCTL
)
from clwpos.data_collector_utils import get_cached_php_installed_versions
from clwpos.logsetup import setup_logging
from clwpos.utils import (
daemon_communicate,
PHP,
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()
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 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(php)) for php in
self.get_supported_php()
}
php_versions_to_enable_redis = [
php for php, redis_data in php_versions_redis_data.items()
if
not redis_data.get('is_present') or not redis_data.get('is_loaded')
]
if not php_versions_to_enable_redis:
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)
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
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
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 unintialized, skipping force-update')
else:
subprocess.run(
[CAGEFSCTL, '--wait-lock', '--force-update'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
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):
""""""
raise NotImplementedError
def redis_package(self, php):
raise NotImplementedError
def redis_ini(self, php_version):
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):
"""
Looks through /opt/cpanel and gets installed phps
"""
base_dir = Path(BASE_CPANEL_EA_PHP_DIR)
minimal_supported = parse_version('ea-php74')
supported = []
for item in os.listdir(base_dir):
if item.startswith('ea-php') and PHP(
item).bin().exists() and parse_version(
item) >= minimal_supported:
supported.append(item)
return supported
def redis_package(self, php):
return f'{php}-php-redis'
def redis_ini(self, php_version):
return Path(PHP(php_version).dir()).joinpath('root/etc/php.d/50-redis.ini')
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):
"""
Looks through /opt/plesk/php and gets installed phps.
/opt/plesk/php contains plain version directories, e.g. 7.4; 8.0; 8.1
"""
base_dir = Path(BASE_PLESK_PHP_DIR)
minimal_supported = parse_version('plesk-php74')
supported = []
for item in os.listdir(base_dir):
_php = f"plesk-php{item.replace('.', '')}"
if PHP(_php).bin().exists() and parse_version(
_php) >= minimal_supported:
supported.append(_php)
return supported
def redis_package(self, php):
return f'{php}-redis'
def redis_ini(self, php_version):
return Path(PHP(php_version).dir()).joinpath(f'etc/php.d/redis.ini')
@lru_cache()
def _redis_extension_info(version: PHP) -> dict:
is_present = bool(list(version.modules_dir().glob("**/redis.so")))
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 redis', shell=True, executable='/bin/bash', env={}
).returncode == 0 if is_present else False
return {
"is_present": is_present,
"is_loaded": 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)