#!/opt/cloudlinux/venv/bin/python3 -bb
# 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
import argparse
import logging
import os
import sys
from cllimits.cagefs_lib import CageFs
from clwpos.constants import ALT_PHP_PREFIX, CLWPOS_VAR_DIR
from clwpos.feature_suites import is_module_visible_for_user
from clwpos.logsetup import setup_logging
from clwpos.object_cache.redis_utils import filter_php_versions_with_not_loaded_redis
from clwpos.optimization_features import OBJECT_CACHE_FEATURE
from clwpos.php.alt_php import (
create_generic_php,
disable_selector_extensions,
get_alt_php_versions,
update_selector_built_in_extensions,
)
from clwpos.php.base import PHP
from clwpos.utils import (
PhpIniConfig,
acquire_lock,
create_pid_file,
)
logger = setup_logging(
caller_name='enable_redis_for_alt_php',
file_level=logging.DEBUG,
logfile_path='/var/log/clwpos/enable_redis_for_alt_php.log',
)
awp_ini = 'acceleratewp.ini'
# ALTPHP-2352 default-extension layout scanned in standalone environments
STANDALONE_INI_DIR = PhpIniConfig.STANDALONE_INI_DIR
# PHP Selector extension sources: cagefs copies every active line of
# php.d.all/.ini into the users' alt_php.ini
SELECTOR_INI_DIR = 'etc/php.d.all'
# marker for those clients, who do not want us modifying global ini
DO_NOT_MODIFY_PHP_GLOBAL_MARKER = f'{CLWPOS_VAR_DIR}/admin/do_not_modify_global_php.flag'
NOT_VISIBLE_MESSAGE = 'Run only if object caching is allowed for at least one user'
def remove_custom_ini(version: str | None, all_ini: bool | None):
if not os.path.exists(DO_NOT_MODIFY_PHP_GLOBAL_MARKER):
logger.debug('Nothing to do')
return
supported_version = supported_alt_php(version)
for php_version in supported_version:
php_ini_config = PhpIniConfig(php_version, custom_logger=logger)
php_ini_config.remove_custom_ini(f'etc/php.d/{awp_ini}', all_ini)
def update_user_ini(php_version: PHP):
php_ini_config = PhpIniConfig(php_version, custom_logger=logger)
modules_to_sync = php_ini_config.get_ini_content(f'etc/php.d/{awp_ini}')
php_ini_config.update_user_ini('acceleratewp.ini', modules_to_sync)
def enable_redis_extension(php_version: PHP):
"""
Enable 'redis' extension for specified alt-php version
by adding 'extension=redis.so' into php.ini file.
Also enable 'json' and 'igbinary' extensions if exist which are reqired by 'redis' extension.
At the same time:
- disable extension in 'php.d.all/.ini' to deny user
to enable extension via PHP Selector.
- disable extensions in 'php.d/default.ini' to eliminate duplicate loading of extensions.
- disable extensions in 'php.d.std/*.ini' for the same reason (see disable_standalone_duplicates).
- disable extensions declared as dependencies in other 'php.d.all/*.ini'
(see disable_selector_dependency_duplicates).
"""
php_ini_config = PhpIniConfig(php_version, custom_logger=logger)
redis_required_modules = php_ini_config.get_required_modules('etc/php.d.all/redis.ini')
if not redis_required_modules:
logger.debug('Redis extension is not present in the system, skip')
return
modules = ['json'] + redis_required_modules
modules.sort(key=lambda x: x.endswith('redis'))
module_status_map = {}
for module in modules:
if module == 'redis':
module_status_map['redis'] = php_ini_config.disable_modules(
'etc/php.d.all/redis.ini', redis_required_modules
)
else:
module_status_map[module] = php_ini_config.disable_modules(f'etc/php.d.all/{module}.ini', [module])
logger.debug(f'Final current modules status: {module_status_map}')
existing_modules = [module for module, exists in module_status_map.items() if exists]
logger.debug(f'Existing extensions: {existing_modules}')
php_ini_config.disable_modules('etc/php.d/default.ini', existing_modules)
# CLOS-6959: php.d.std/ (ALTPHP-2352) is one more place where the same
# extensions may be declared; php.ini stays the single active declaration
php_ini_config.disable_modules_in_dir(STANDALONE_INI_DIR, existing_modules)
non_loaded_modules = [module for module in existing_modules if not php_version.is_extension_loaded(module)]
if not os.path.exists(DO_NOT_MODIFY_PHP_GLOBAL_MARKER):
php_ini_config.enable_modules('etc/php.ini', non_loaded_modules)
else:
logger.debug(
'Marker exists: %s, adding those modules to custom awp ini: %s',
DO_NOT_MODIFY_PHP_GLOBAL_MARKER,
str(non_loaded_modules),
)
php_ini_config.create_custom_ini(f'etc/php.d/{awp_ini}', non_loaded_modules)
disable_selector_dependency_duplicates(php_version)
disable_selector_extensions(non_loaded_modules, php_version.version, logger)
def _get_managed_modules(php_version: PHP, php_ini_config: PhpIniConfig) -> list[str]:
"""
Return the extensions AccelerateWP moves into php.ini for the version:
redis with its dependencies, plus json where it is a shared object.
Empty when redis is not installed for the version.
"""
redis_required_modules = php_ini_config.get_required_modules('etc/php.d.all/redis.ini')
if not redis_required_modules:
return []
managed_modules = list(redis_required_modules)
# json is a shared object only on older alt-php (php.d.all/json.ini present);
# there enable_redis_extension() moves it into php.ini as well
if os.path.exists(os.path.join(php_version.dir, 'etc/php.d.all/json.ini')):
managed_modules.append('json')
return managed_modules
def disable_standalone_duplicates(php_version: PHP, owned_only: bool = False) -> bool:
"""
Keep php.ini the single active declaration of the extensions AccelerateWP
manages (igbinary, msgpack, redis and json where it is a shared object).
:param owned_only: touch only the modules AccelerateWP already declares
actively in php.ini (or php.d/acceleratewp.ini on servers with the
DO_NOT_MODIFY_PHP_GLOBAL_MARKER). Used when object cache is not visible
for any user: the declarations we wrote earlier stay in php.ini, so a
later alt-php pecl update would otherwise leave the 'already loaded'
warning behind for good - but we must not take over declarations on a
server where AccelerateWP never configured this version (redis may
legitimately be loaded from php.d.std/ alone there).
Since ALTPHP-2352 the alt-php pecl packages ship
/opt/alt/phpXY/etc/php.d.std/-.ini with an active 'extension=' line.
The interpreter scans php.d.std/ (in addition to link/conf) when the
/opt/alt/php-standalone marker exists and the process is not under
PHP Selector (link/conf -> /etc/cl.php.d/...). AccelerateWP enables the
extensions in php.ini because that is the only location read in every
environment, including PHP Selector users inside CageFS where php.d.std/
is never scanned - so the php.ini lines must stay. The php.d.std/ copies
are redundant there and make PHP print
'Module "" is already loaded in Unknown on line 0' on every startup
(CLOS-6959). Comment them out the same way we already do for
php.d.all/.ini and php.d/default.ini.
This has to run on every invocation (yum/dnf hook, daily cron, package
posttrans) and not only on the first configuration of a PHP version:
the duplicates arrive with an alt-php package update long after redis
was configured. It also has to run BEFORE the 'is redis loaded' check,
otherwise a freshly installed alt-php on a standalone server looks
configured to root (redis loaded via php.d.std/) while PHP Selector users
never get the extension.
:return: True when at least one php.d.std/ file was changed
"""
php_ini_config = PhpIniConfig(php_version, custom_logger=logger)
managed_modules = _get_managed_modules(php_version, php_ini_config)
if not managed_modules:
# redis is not installed for this version, nothing is managed by us
return False
if owned_only:
owned_modules = php_ini_config.get_enabled_modules('etc/php.ini') | php_ini_config.get_enabled_modules(
f'etc/php.d/{awp_ini}'
)
managed_modules = [module for module in managed_modules if module in owned_modules]
if not managed_modules:
logger.debug('%s: no extension declared by AccelerateWP, nothing to reconcile', php_version.identifier)
return False
disabled = php_ini_config.disable_modules_in_dir(STANDALONE_INI_DIR, managed_modules)
if disabled:
logger.info(
'%s: disabled duplicate extension declarations in %s: %s',
php_version.identifier,
STANDALONE_INI_DIR,
disabled,
)
return bool(disabled)
def disable_selector_dependency_duplicates(php_version: PHP) -> bool:
"""
Comment out the dependency lines for the extensions AccelerateWP loads
from php.ini in every php.d.all/*.ini (CLOS-7527).
Other pecl packages declare our extensions as their dependency, e.g.
alt-phpXY-pecl-memcached ships php.d.all/memcached.ini with
'extension=igbinary.so' before 'extension=memcached.so'. cagefs builds a
PHP Selector user's alt_php.ini from the active lines of php.d.all/.ini
and skips a line only when it is already written, so with igbinary.ini
disabled by enable_redis_extension() the igbinary line of memcached.ini
lands in alt_php.ini. igbinary is then loaded from both php.ini and
alt_php.ini and PHP prints 'Module "igbinary" is already loaded' on every
startup. The same applies to json on alt-php 7.2-7.4, where it is a shared
object loaded from php.ini and declared as a dependency in solr.ini,
mongodb.ini, apm.ini and phalcon*.ini.
Only modules with an active line in php.ini are touched: php.ini is read
for every PHP Selector user and loads before the scan directory, so memcached
still finds igbinary. php.d/acceleratewp.ini (DO_NOT_MODIFY_PHP_GLOBAL_MARKER)
is not counted, it reaches a user only after update_user_ini() and a user
without it would lose igbinary and fail to load memcached. Servers where
AccelerateWP never configured the version keep php.d.all as shipped.
Runs on every invocation (like disable_standalone_duplicates) because
a pecl package may be installed or updated after igbinary moved to php.ini.
The caller must rebuild the users' alt_php.ini when files were changed.
:return: True when at least one php.d.all/ file was changed
"""
php_ini_config = PhpIniConfig(php_version, custom_logger=logger)
loaded_by_php_ini = php_ini_config.get_enabled_modules('etc/php.ini')
modules = [module for module in _get_managed_modules(php_version, php_ini_config) if module in loaded_by_php_ini]
if not modules:
return False
disabled = php_ini_config.disable_modules_in_dir(SELECTOR_INI_DIR, modules)
if disabled:
logger.info(
'%s: disabled duplicate extension declarations in %s: %s',
php_version.identifier,
SELECTOR_INI_DIR,
disabled,
)
return bool(disabled)
def rebuild_users_alt_php_ini(cagefs: CageFs, no_rebuild: bool):
"""
Rebuild users' alt_php.ini if all condition are satisfied:
- script called with no flag --no-rebuild
- cagefs package is installed
"""
if not no_rebuild and cagefs.is_cagefs_present():
cagefs.rebuild_alt_php_ini()
update_selector_built_in_extensions(logger)
def rebuild_after_selector_reconcile(cagefs: CageFs, no_rebuild: bool):
"""
Deliver php.d.all/ changes of disable_selector_dependency_duplicates() to
PHP Selector users when main() has nothing else to do. A failed rebuild is
logged and does not abort the yum hook / cron run.
"""
try:
rebuild_users_alt_php_ini(cagefs, no_rebuild)
except Exception:
logger.exception("Failed to rebuild users' alt_php.ini after %s changes", SELECTOR_INI_DIR)
def supported_alt_php(version: str | None):
alt_php_versions = [create_generic_php(f'{ALT_PHP_PREFIX}{version}')] if version else get_alt_php_versions()
supported_alt_php_versions = [
version for version in alt_php_versions if OBJECT_CACHE_FEATURE.is_php_supported(version)
]
return supported_alt_php_versions
def get_php_versions_to_enable_redis(version: str | None) -> list[PHP]:
"""
Return list of php version supported by AccelerateWP
and on which redis is presented but not enabled.
"""
supported_alt_php_versions = supported_alt_php(version)
return filter_php_versions_with_not_loaded_redis(supported_alt_php_versions)
def parse_args() -> argparse.Namespace:
"""
Parse arguments for enable_redis_for_alt_php.py utility.
"""
parser = argparse.ArgumentParser(
prog='enable_redis_for_alt_php',
description='Utility for enabling redis extension for alt-php',
)
parser.add_argument(
'--version',
type=int,
help='Enable redis extension for specific alt-php version',
)
parser.add_argument(
'--no-rebuild',
action='store_true',
help='Run without rebuilding users\' alt_php.ini files',
)
parser.add_argument('--remove-custom-ini', action='store_true', help='Run to remove custom acceleratewp.ini')
parser.add_argument('--all', action='store_true', help='Run to remove ALL custom acceleratewp.ini')
parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-f', '--force', action='store_true')
opts, _ = parser.parse_known_args()
if opts.verbose:
logging.basicConfig(level=logging.DEBUG)
return opts
def main():
"""
Enable redis extension for alt-php versions supported by AccelerateWP.
"""
opts = parse_args()
# with object cache visible for at least one user (or --force) the utility
# may enable redis and take over its declarations for every supported version
object_cache_visible = opts.force or is_module_visible_for_user(OBJECT_CACHE_FEATURE)
if opts.remove_custom_ini:
if not object_cache_visible:
print(NOT_VISIBLE_MESSAGE)
sys.exit(0)
remove_custom_ini(opts.version, opts.all)
sys.exit(0)
cagefs = CageFs()
with acquire_lock(os.path.join('/var/run', ALT_PHP_PREFIX), attempts=None):
# CLOS-6959: reconcile php.d.std/ duplicates for every supported version
# on every run, before deciding whether redis still has to be enabled.
# This also runs when object cache is not visible for anybody anymore:
# the php.ini lines written earlier stay, and an alt-php pecl update
# would otherwise leave the 'already loaded' warning behind for good.
# In that state only the modules we already load are touched.
# CLOS-7527: the same for dependency lines in php.d.all/, which reach
# PHP Selector users only after their alt_php.ini is rebuilt.
selector_ini_changed = False
for php_version in supported_alt_php(opts.version):
try:
disable_standalone_duplicates(php_version, owned_only=not object_cache_visible)
except Exception:
logger.exception('Failed to reconcile %s duplicates for %s', STANDALONE_INI_DIR, php_version.identifier)
try:
if disable_selector_dependency_duplicates(php_version):
selector_ini_changed = True
except Exception:
logger.exception('Failed to reconcile %s duplicates for %s', SELECTOR_INI_DIR, php_version.identifier)
if not object_cache_visible:
if selector_ini_changed:
rebuild_after_selector_reconcile(cagefs, opts.no_rebuild)
print(NOT_VISIBLE_MESSAGE)
sys.exit(0)
php_versions_to_enable_redis = get_php_versions_to_enable_redis(opts.version)
# we don't continue if there are no alt-php versions (supported by WPOS)
# with redis presented but not loaded
if not php_versions_to_enable_redis:
if selector_ini_changed:
rebuild_after_selector_reconcile(cagefs, opts.no_rebuild)
logger.info('All php versions have redis extension installed and active')
sys.exit(0)
with create_pid_file(ALT_PHP_PREFIX):
for php_version in php_versions_to_enable_redis:
enable_redis_extension(php_version)
# iter all supported, to sync users ini in case we already enabled in global .ini
if os.path.exists(DO_NOT_MODIFY_PHP_GLOBAL_MARKER):
for php_version in supported_alt_php(None):
update_user_ini(php_version)
# we don't continue if there are no alt-php versions (supported by WPOS)
# with redis presented but not loaded
if not php_versions_to_enable_redis:
sys.exit(0)
rebuild_users_alt_php_ini(cagefs, opts.no_rebuild)
if __name__ == '__main__':
main()