Mini Shell
# -*- 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 clcommon.utils import is_ubuntu
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
env = os.environ.copy()
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)
command_to_install = ['yum', '-y', 'install', *self._additional_repos, redis_package]
if is_ubuntu():
command_to_install = ['apt-get', '-y', 'install', redis_package]
env.update({'DEBIAN_FRONTEND': 'noninteractive'})
_logger.info('Trying to install package=%s via command=%s',
redis_package,
str(command_to_install))
result = subprocess.run(
command_to_install,
capture_output=True,
text=True,
env=env
)
if result.returncode != 0 and ('Nothing to do' not in result.stdout
and 'is already the newest version' 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)