⬆️ Go up: /usr
#!/usr/bin/python3
# -*- mode:python; coding:utf-8; -*-
# author: Eugene Zamriy <ezamriy@cloudlinux.com>,
# Sergey Fokin <sfokin@cloudlinux.com>
# editor: Eduard Chiganov <echiganov@cloudlinux.com>
# created: 29.07.2015 15:46
# edited: Eduard Chiganov 05.2025 16:20
# description: Selects correct alt-php MySQL binding according to the system
# configuration.
import getopt
import glob
import logging
import os
import platform
import re
import subprocess
import sys
import traceback
from decimal import Decimal
try:
import rpm
except:
class rpm:
RPMMIRE_REGEX = None
class pattern:
def __init__(self, packages):
self.packages = packages
def pattern(self, field, flag, pattern):
regexp = re.compile(pattern)
self.packages = list(filter(regexp.match, self.packages))
def __getitem__(self, item):
return self.packages[item]
class TransactionSet:
@staticmethod
def dbMatch():
return rpm.pattern(os.popen('rpm -qa').readlines())
VER_PATTERNS = {"18.1": "5.6", "18.1.0": "5.6", "18.0": "5.5", "18.0.0": "5.5",
"18": "5.5", "16": "5.1", "15": "5.0", "20.1": "5.7",
"20.2": "5.7", "20.3": "5.7", "21.0": "8.0", "21.2": "8.0", "24.0.5": "8.4"}
def is_debian():
"""
Check if we running on Debian/Ubuntu
@rtype : bool
@return True or False
"""
if os.path.exists("/etc/redhat-release"):
return False
return True
def is_plesk():
"""
Check is it environment with installed plesk panel
@rtype : bool
@return True or False
"""
if not os.path.exists("/usr/sbin/plesk"):
return False
return True
def has_cagefs():
"""
Check if we're in environment with enabled cagefs
@rtype : bool
@return True or False
"""
if not os.path.exists("/usr/sbin/cagefsctl"):
return False
with open(os.devnull, 'wb') as devnull:
result = subprocess.call(
["/usr/sbin/cagefsctl", "--cagefs-status"],
stdout=devnull,
stderr=devnull
)
return result == 0
def is_bare_plesk():
"""
Check is it environment with installed plesk panel on clean ELS system without cagefs
@rtype : bool
@return True or False
"""
return is_plesk() and not has_cagefs()
def configure_logging(verbose):
"""
Logging configuration function.
@type verbose: bool
@param verbose: Enable additional debug output if True, display only errors
otherwise.
"""
if verbose:
level = logging.DEBUG
else:
level = logging.ERROR
handler = logging.StreamHandler()
handler.setLevel(level)
log_format = "%(levelname)-8s: %(message)s"
formatter = logging.Formatter(log_format, "%H:%M:%S %d.%m.%y")
handler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(level)
return logger
def symlink_abs_path(path):
"""
Recursively resolves symlink.
@type path: str
@param path: Symlink path.
@rtype: str
@return: Resolved symlink absolute path.
"""
processed_symlinks = set()
if not isinstance(path, str):
return None
while os.path.islink(path):
if path in processed_symlinks:
return None
path = os.path.join(os.path.dirname(path), os.readlink(path))
processed_symlinks.add(path)
return os.path.abspath(path)
def create_symlink_to_mysqli_ini(source_dir, target_dir, int_ver=None):
"""
Makes php.d/mysqli.ini current for one alt-php version.
php<82: a symlink at the php.d.all copy, as it has always been.
php82+: a real file, because the two copies must differ (see int_ver).
@type source_dir: str
@param source_dir: php.d.all - holds the authoritative mysqli.ini body.
@type target_dir: str
@param target_dir: php.d - where the active copy is created or updated.
@type int_ver: str
@param int_ver: alt-php version (e.g. "82"). On php82+ the two copies of
mysqli.ini deliberately differ (ALTEAPLN-53): php.d.all declares
extension=mysqlnd.so for the PHP Selector, php.d must not because
MYSQLND_INI_NAME already loads it there. A symlink cannot express that,
so on php82+ this writes the php.d body instead of linking - see the
branch below. Unset only by callers that cannot have a version in hand,
which then get the historical symlink behaviour.
@rtype: bool
@return: True if php.d/mysqli.ini is correct afterwards, False on error or
when skipped on bare Plesk.
"""
source_path = os.path.join(source_dir, 'mysqli.ini')
target_path = os.path.join(target_dir, 'mysqli.ini')
try:
ver_num = int(int_ver)
except (TypeError, ValueError):
ver_num = 0
if is_bare_plesk():
return False
if ver_num >= 82:
# ALTEAPLN-53: php.d must hold the alias body WITHOUT the
# extension=mysqlnd.so that php.d.all carries for the Selector -
# MYSQLND_INI_NAME owns mysqlnd here, and a second declaration makes PHP
# warn 'Module "mysqlnd" is already loaded'. A symlink cannot express
# that, so write the file.
#
# Rewrite rather than just skip: this is reached from the bare
# (non---driver) reconfigure that alt-php-config's %triggerin runs for
# every installed version whenever an alt-phpXY-mysqlnd package changes.
# An older alt-php-config running against php.d.all bodies from
# mysql-meta 3-28 leaves a symlink here, and only that version's own
# mysql-meta posttrans would otherwise repair it - so a rollback of
# alt-php-config alone would silently reinstate the duplicate warning.
# Deriving the body from php.d.all keeps this driver-agnostic.
try:
with open(source_path) as fh:
body = fh.read()
except (IOError, OSError) as e:
logging.error(u"cannot read %s: %s" % (source_path, e))
return False
active = "".join(l + "\n" for l in body.splitlines()
if l.strip() != "extension=mysqlnd.so")
try:
if os.path.islink(target_path) or os.path.exists(target_path):
with open(target_path) as fh:
# ALTPHP-2624: lines yielded to default.ini still count
# as correct; yield_to_default_ini() keeps them current
if not os.path.islink(target_path) and \
unyield_body(fh.read()) == active:
return True # already correct, leave mtime alone
os.remove(target_path)
with open(target_path, "w") as fh:
fh.write(active)
except (IOError, OSError) as e:
logging.error(u"cannot write %s: %s" % (target_path, e))
return False
logging.info(u"php%s: %s written (php.d copy, mysqlnd owned by %s)"
% (int_ver, target_path, MYSQLND_INI_NAME))
return True
else:
try:
# Remove the target file/symlink if it exists (mimic --force)
if os.path.exists(target_path) and not os.path.islink(target_path):
os.remove(target_path)
if source_path == symlink_abs_path(target_path):
logging.debug(u"%s is already configured to %s" % (target_path, source_path))
return True
# Create the symlink
os.symlink(source_path, target_path)
logging.info(u"Symlink created or updated: %s -> %s" % (target_path, source_path))
return True
except Exception as e:
logging.error(u"Error creating or updating symlink: %s" % str(e))
return False
def find_interpreter_versions(interpreter="php"):
"""
Returns list of installed alt-php versions and their base directories.
@rtype: list
@return: List of version (e.g. 44, 55) and base directory tuples.
"""
int_versions = []
if interpreter == "ea-php":
base_path_regex = "/opt/cpanel/ea-php[0-9][0-9]/root/"
else:
base_path_regex = "/opt/alt/%s[0-9][0-9]" % interpreter
for int_dir in glob.glob(base_path_regex):
int_versions.append((int_dir[-2:], int_dir))
int_versions.sort()
return int_versions
def find_mysql_executable(mysql="mysql"):
"""
Detects MySQL binary full path.
@type mysql: str
@param mysql: MySQL binary name (default is "mysql").
@rtype: str or None
@return: MySQL binary full path or None if nothing is found.
"""
for path in os.environ["PATH"].split(os.pathsep):
mysql_path = os.path.join(path, mysql)
if os.path.exists(mysql_path) and os.access(mysql_path, os.X_OK):
if os.path.islink(mysql_path):
return os.readlink(mysql_path)
else:
return mysql_path
def is_percona(major, minor):
"""
Check if Percona server is installed
@type major: str
@param major: major version of sql server
@type minor: str
@param minor: minor version of sql server
@rtype: bool
@return: True or False
"""
if is_debian():
if not os.system("dpkg -l | grep -i percona-server"):
return True
else:
ts = rpm.TransactionSet()
mi = ts.dbMatch()
pattern = "Percona-Server-shared-{0}{1}|cl-Percona{0}{1}-shared".format(
major, minor)
mi.pattern('name', rpm.RPMMIRE_REGEX, pattern)
for _ in mi:
mysql_type = "percona"
return True
return False
def parse_mysql_version(version):
"""
Extracts MySQL engine type and version from the version string
(mysql -V output).
@type version: str
@param version: MySQL version string (mysql -V output).
@rtype: tuple
@return: MySQL engine type (e.g. mariadb, mysql) and version (e.g.
5.6, 10.0) tuple.
"""
ver_rslt = ""
if re.search(r"mysql", version, re.IGNORECASE):
ver_rslt = re.search(r"mysql\s+Ver\s+.*?Distrib\s+((\d+)\.(\d+)\S*?),?\s+for",
version)
if not ver_rslt:
ver_rslt = re.search(r"mysql\s+Ver\s+((\d+)\.(\d+)\S*)", version)
else:
ver_rslt = re.search(r"mariadb\s+from\s+((\d+)\.(\d+)\S*?),?\s+", version)
if not ver_rslt:
ver_rslt = re.search(r"mariadb\s+Ver\s+.*?Distrib\s+((\d+)\.(\d+)\S*?),?"
r"\s+for", version)
if not ver_rslt:
return None, None
full_ver, major, minor = ver_rslt.groups()
mysql_type = "mysql"
mysql_ver = "%s.%s" % (major, minor)
if re.search(r"mariadb", full_ver, re.IGNORECASE):
mysql_type = "mariadb"
# NOTE: there are no way to detect Percona by "mysql -V" output, so we
# are looking for Percona-Server-shared* or cl-Percona*-shared package installed
if is_percona(major, minor):
mysql_type = "percona"
return mysql_type, mysql_ver
def get_mysql_version(mysql_path):
"""
Returns MySQL engine type and version of specified MySQL executable.
@type mysql_path: str
@param mysql_path: MySQL executable path.
@rtype: tuple
@return: MySQL engine type (mariadb or mysql) and version (e.g.
5.6, 10.0) tuple.
"""
proc = subprocess.Popen([mysql_path, "-V"], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, universal_newlines=True)
out, _ = proc.communicate()
if proc.returncode != 0:
raise Exception(u"cannot execute \"%s -V\": %s" % (mysql_path, out))
ver_string = out.strip()
logging.debug(u"SQL version string is '%s'" % ver_string)
return parse_mysql_version(ver_string)
def detect_so_version(so_path):
"""
Parameters
----------
so_path : str or unicode
Absolute path to .so library
Returns
-------
tuple
Tuple of MySQL type name and MySQL version
"""
mysql_ver = None
for ver_pattern in VER_PATTERNS:
if re.search(re.escape(".so.%s" % ver_pattern), so_path):
mysql_ver = VER_PATTERNS[ver_pattern]
if is_debian():
mysql_ver = mysql_ver.replace(".", "")
# in some Percona builds .so was renamed to libperconaserverclient.so
if "libperconaserverclient.so" in so_path:
return "percona", mysql_ver
# search for markers (mariadb/percona) in .so strings
proc = subprocess.Popen(["strings", so_path], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, universal_newlines=True)
out, _ = proc.communicate()
if proc.returncode != 0:
raise Exception(u"cannot execute \"strings %s\": %s" % (so_path, out))
mysql_type = "mysql"
for line in out.split("\n"):
if re.search(r"percona", line, re.IGNORECASE):
return "percona", mysql_ver
maria_version = re.search(r"^(1[0-1]\.[0-9]+)\.[0-9]*(-MariaDB)?$", line,
re.IGNORECASE)
if maria_version is not None and len(maria_version.groups()) != 0:
return "mariadb", maria_version.group(1)
if re.search(r"5\.5.*?-MariaDB", line, re.IGNORECASE):
return "mariadb", "5.5"
if re.search(r"mariadb", line, re.IGNORECASE):
mysql_type = "mariadb"
return mysql_type, mysql_ver
def detect_lib_dir():
"""
Returns
-------
str
lib if running on 32-bit system, lib64 otherwise
"""
if is_debian():
# ALTPHP-2242: the multiarch triplet is arch-specific — hardcoding
# x86_64-linux-gnu breaks ARM64 (needs aarch64-linux-gnu). Derive it from
# the running machine so the module path is correct on amd64 and arm64.
return "lib/%s-linux-gnu" % os.uname()[4]
if platform.architecture()[0] == "64bit":
return "lib64"
else:
return "lib"
def get_int_files_root_path(int_name, int_ver):
"""
Parameters
----------
int_name : str or unicode
Interpreter name (php, python)
int_ver : str or unicode
Interpreter version (44, 70, 27, etc.)
Returns
-------
str
Absolute path to interpreter root
"""
if int_name == "php":
return "/opt/alt/php%s" % int_ver
elif int_name == "ea-php":
return "/opt/cpanel/ea-php%s/root/" % int_ver
elif int_name == "python":
return "/opt/alt/python%s" % int_ver
else:
raise NotImplementedError("Unknown interpreter")
def get_dst_so_path(int_name, int_ver, so_name):
"""
Parameters
----------
int_name : str or unicode
Interpreter name (php, python)
int_ver : str or unicode
Interpreter version (44, 70, 27, etc.)
so_name : str or unicode
MySQL shared library name
Returns
-------
str
Absolute path to MySQL binding destination point
"""
lib_dir = detect_lib_dir()
int_path = get_int_files_root_path(int_name, int_ver)
int_dot_ver = "%s.%s" % (int_ver[0], int_ver[-1])
if int_name in ["php", "ea-php"]:
if re.match(r".*_ts.so", so_name):
return os.path.join(int_path, "usr", lib_dir, "php-zts/modules", re.sub(r'_ts\.so', '.so', so_name))
else:
return os.path.join(int_path, "usr", lib_dir, "php/modules", so_name)
elif int_name == "python":
if os.path.exists("/opt/alt/python{0}/bin/python{1}".format(int_ver, int_ver[0])):
proc = subprocess.Popen("/opt/alt/python{0}/bin/python{1} -c \"from distutils.sysconfig import get_python_lib; print(get_python_lib(True))\"".format(int_ver, int_ver[0]),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True)
out, _ = proc.communicate()
return os.path.join(out.strip(),so_name)
else:
raise NotImplementedError("Unknown interpreter")
def get_mysql_pkg_name(int_name, int_ver, mysql_type, mysql_ver, zts=False):
"""
Parameters
----------
int_name : str or unicode
Interpreter name (php, python)
int_ver : str or unicode
Interpreter version (44, 27, 71, etc.)
mysql_type : str or unicode
Mysql base type (mysql, mariadb, percona)
mysql_ver : str or unicode
Mysql version (5.5, 10, 10.1)
Returns
-------
"""
if int_name == "php":
if not zts:
return "alt-php%s-%s%s" % (int_ver, mysql_type, mysql_ver)
else:
return "alt-php%s-%s%s-zts" % (int_ver, mysql_type, mysql_ver)
elif int_name == "ea-php":
return "%s%s-php-%s%s" % (int_name, int_ver, mysql_type, mysql_ver)
elif int_name == "python":
return "alt-python%s-MySQL-%s%s" % (int_ver, mysql_type, mysql_ver)
else:
raise NotImplementedError("Unknown interpreter")
def get_so_list(int_name):
"""
Parameters
----------
int_name : str
Interpreter name (e.g. php, python, etc.)
Returns
-------
list
Binding .so names reconfigure manages for the interpreter.
"""
if int_name == "ea-php":
return ["mysql.so", "mysqli.so", "pdo_mysql.so"]
elif int_name == "php":
# ALTPHP-2242: client-lib bindings stay available for every version;
# the active driver (native nd_* vs binding) is chosen via --driver.
# reconfigure keeps the binding .so symlinks fresh regardless of mode.
if is_bare_plesk():
return ["mysql.so", "mysqli.so", "pdo_mysql.so"]
return ["mysql.so", "mysqli.so", "pdo_mysql.so", "mysql_ts.so",
"mysqli_ts.so", "pdo_mysql_ts.so"]
elif int_name == "python":
return ["_mysql.so"]
else:
raise NotImplementedError("Unknown interpreter")
def match_so_to_mysql():
mysql = find_mysql_executable()
# If we have no MySQL, then nothing should be done
if not mysql:
return
possible_versions = list(VER_PATTERNS.values())
possible_versions += ["10", "10.0", "10.1", "10.2", "10.3", "10.4", "10.5",
"10.6", "10.11", "11.4","11.04", "11.8"]
mysql_type, mysql_ver = get_mysql_version(mysql)
if is_debian():
mysql_ver = mysql_ver.replace(".", "")
if mysql_type not in ["mysql", "mariadb", "percona"] or \
mysql_ver not in possible_versions:
return
if mysql_ver == "5.0":
search_pattern = re.compile(r"(\S*libmysqlclient\.so\.15\S*)")
elif mysql_ver == "5.1":
search_pattern = re.compile(r"(\S*libmysqlclient\.so\.16\S*)")
elif mysql_ver in ("5.5", "10", "10.0", "10.1"):
search_pattern = re.compile(r"(\S*libmysqlclient\.so\.18\.0\S*)")
elif mysql_ver == "5.6":
search_pattern = re.compile(r"(\S*libmysqlclient\.so\.18\.1\S*)")
elif mysql_ver == "5.7":
search_pattern = re.compile(r"(\S*libmysqlclient\.so\.20\S*)")
elif mysql_ver == "8.0":
search_pattern = re.compile(r"(\S*libmysqlclient\.so\.21\S*)")
elif mysql_ver == "8.4":
search_pattern = re.compile(r"(\S*libmysqlclient\.so\.24\S*)")
elif mysql_ver in ("10.2", "10.3", "10.4", "10.5", "10.6", "10.11", "11.04", "11.4", "11.8"):
search_pattern = re.compile(r"(\S*libmariadb\.so\.3\S*)")
else:
raise Exception(u"Cannot match MySQL library to any version")
if mysql_type == "percona":
search_path = ["/usr/%s" % detect_lib_dir()]
else:
search_path = ["/usr/local/mysql/lib/", # Added path for Direect Admin
"/usr/%s/" % detect_lib_dir(),
"/usr/%s/mysql" % detect_lib_dir(),
"/usr/%s/mariadb" % detect_lib_dir()]
files = []
for libs_path in search_path:
if os.path.exists(libs_path):
for file in os.listdir(libs_path):
files.append(os.path.join(libs_path, file))
for one_file in files:
if search_pattern.match(one_file):
return (search_pattern.match(one_file).string,
mysql_type, mysql_ver)
def get_mysql_so_files():
proc = subprocess.Popen(["/sbin/ldconfig", "-p"], stdout=subprocess.PIPE,
universal_newlines=True)
out, _ = proc.communicate()
if proc.returncode != 0:
raise Exception(u"cannot execute \"ldconfig -p\": %s" % out)
so_re = re.compile(r"^.*?=>\s*(\S*?(libmysqlclient|"
r"libmariadb|"
r"libperconaserverclient)\.so\S*)")
forced_so_file = match_so_to_mysql()
if forced_so_file:
so_files = [forced_so_file]
else:
so_files = []
for line in out.split("\n"):
re_rslt = so_re.search(line)
if not re_rslt:
continue
so_path = symlink_abs_path(re_rslt.group(1))
if not so_path or not os.path.exists(so_path):
continue
mysql_type, mysql_ver = detect_so_version(so_path)
so_rec = (so_path, mysql_type, mysql_ver)
if so_rec not in so_files:
so_files.append(so_rec)
return so_files
def reconfigure_mysql(int_ver, mysql_type, mysql_ver, force=False,
int_name="php"):
"""
Parameters
----------
int_ver : str or unicode
Interpreter version (44, 70, 27, etc.)
mysql_type : str or unicode
MySQL type (mysql, mariadb, percona)
mysql_ver : str or unicode
MySQL version (5.5, 10.1, etc.)
force : bool
Force symlink reconfiguration if True, do nothing otherwise
int_name : str or unicode
Optional, defines interpreter name (php, python). Default is php
Returns
-------
bool
True if reconfiguration was successful, False otherwise
"""
int_dir = get_int_files_root_path(int_name, int_ver)
if is_bare_plesk():
so_list = get_so_list(int_name)
for so_name in so_list:
src_so = get_dst_so_path(int_name, int_ver, "nd_%s" % so_name)
dst_so = get_dst_so_path(int_name, int_ver, so_name)
if os.path.exists(src_so):
try:
if os.path.exists(dst_so):
os.remove(dst_so)
os.symlink(src_so, dst_so)
except Exception as e:
logging.error(u"Error creating symlink: %s" % str(e))
return True
if mysql_type == "mariadb":
if mysql_ver in ("10", "10.0"):
mysql_ver = "10"
elif mysql_ver.startswith("10."):
mysql_ver = mysql_ver.replace(".", "")
elif mysql_ver.startswith("11."):
# ALTPHP-2242: MariaDB 11.x binding dir/package names are INCONSISTENT
# upstream and no single transform works: 11.4 -> mariadb1104
# (zero-padded minor) but 11.8 -> mariadb118 (unpadded). Both ship on
# CL9/CL10. Use an explicit map; fall back to the dot-stripped form for
# any future 11.x not listed here.
mariadb_11x_map = {"11.4": "1104", "11.8": "118"}
mysql_ver = mariadb_11x_map.get(mysql_ver, mysql_ver.replace(".", ""))
elif mysql_ver == "5.5":
# NOTE: there are no special bindings for MariaDB 5.5 in Cloud Linux
# so we are using the MySQL one
mysql_type = "mysql"
so_list = get_so_list(int_name)
for so_name in so_list:
src_so = os.path.join(int_dir, "etc",
"%s%s" % (mysql_type, mysql_ver), so_name)
if not os.path.exists(src_so):
if (so_name in ("mysqli.so", "pdo_mysql.so") and int_ver == "44") \
or (so_name == "mysql.so" and int_ver.startswith("7")) \
or (so_name == "mysql.so" and int_ver.startswith("8")) \
or (re.match(r".*_ts.so", so_name) and int_ver != 72):
# NOTE: there are no mysql.so for alt-php7X and mysqli.so /
# pdo_mysql.so for alt-php44
continue
# TODO: maybe find an appropriate replacement for missing
# .so in other alt-php-(mysql|mariadb|percona) packages?
mysql_pkg_name = get_mysql_pkg_name(int_name, int_ver, mysql_type,
mysql_ver,
bool(re.match(r".*_ts.so",
so_name)))
logging.debug(u"%s is not found. Please install "
u"%s package" % (so_name, mysql_pkg_name))
return False
dst_so = get_dst_so_path(int_name, int_ver, so_name)
dst_so_real = symlink_abs_path(dst_so)
if src_so == dst_so_real:
logging.debug(u"%s is already updated" % dst_so)
continue
else:
force = True
if not isinstance(dst_so, str):
return False
if os.access(dst_so, os.R_OK):
# seems alt-php is already configured - don't touch without force
# argument
if not force:
logging.debug(u"current %s configuration is ok (%s)" %
(dst_so, dst_so_real))
continue
os.remove(dst_so)
os.symlink(src_so, dst_so)
logging.info(u"%s was reconfigured to %s" % (dst_so, src_so))
else:
# seems current alt-php configuration is broken, reconfigure it
try:
os.remove(dst_so)
except:
pass
os.symlink(src_so, dst_so)
logging.info(u"%s was configured to %s" % (dst_so, src_so))
continue
return True
def check_alt_path_exists(int_path, int_name, int_ver):
"""
Parameters
----------
int_path : str or unicode
Interpreter directory on the disk (/opt/alt/php51, etc.)
int_name : str or unicode
Interpreter name (php, python)
int_ver : str or unicode
Interpreter version (44, 70, 27, etc.)
Returns
-------
bool
True if interpreter path exists, False otherwise
"""
if not os.path.isdir(int_path):
sys.stderr.write("unknown {0} version {1}".format(int_name, int_ver))
return False
return True
# ALTPHP-2242: .ini bodies for switching the active mysql driver. "native"
# loads the mysqlnd-based nd_* extensions (no client lib); "binding" loads the
# client-lib .so managed by the reconfigure symlinks.
#
# ALTEAPLN-53: there are two audiences for these files and they need different
# bodies, so we write both instead of symlinking php.d at the php.d.all copy.
#
# php.d.all - read ONLY by the PHP Selector / CageFS per-user generators
# (cagefslib.get_dependencies, clselect.clextselect and
# cpanel/lib/extensions.process_dependencies). They build each
# user's alt_php.ini from php.d.all alone and derive the load
# order purely from the `extension=<dep>.so` lines in these
# bodies. A dependency that is not declared here does not exist
# as far as they are concerned - php.d is never read, so
# MYSQLND_INI_NAME below is invisible to all three. All three
# also look a dependency up as php.d.all/<so basename>.ini, so
# the owner file has to be named exactly mysqlnd.ini (it is,
# from alt-phpXY-mysqlnd) - a 00- prefixed name would never be
# found. Hence: php82+ bodies MUST declare extension=mysqlnd.so.
#
# php.d - what the native/root PHP actually scans, alphabetically.
# MYSQLND_INI_NAME owns mysqlnd there, so an alias repeating it
# makes PHP print 'Module "mysqlnd" is already loaded in Unknown
# on line 0' on every invocation (ALTEAPLN-12). Hence: no alias
# body written to php.d may declare it.
#
# ALTEAPLN-12 collapsed the two by dropping mysqlnd from both bodies. That
# silenced the warning but deleted the only mysqli/pdo_mysql -> mysqlnd edge
# the Selector could see, so every account whose module list carries the alias
# names got nd_mysqli.so loaded before mysqlnd.so and mysqli died with
# 'undefined symbol: mysqlnd_global_stats' (ALTPHP-2598, ALTEAPLN-53).
#
# Keep the php.d.all bodies byte-identical to the ones alt-phpXY-mysql-meta
# ships: they are config(noreplace) there, and any drift makes every upgrade
# leave an .rpmnew behind.
# Bodies written to php.d.all (the Selector/CageFS dependency graph).
DRIVER_INI = {
"native": {
"mysqli.ini": "; Enable mysqli native driver extension\n"
"extension=mysqlnd.so\nextension=nd_mysqli.so\n",
"pdo_mysql.ini": "; Enable pdo_mysql native driver extension\n"
"extension=pdo.so\nextension=mysqlnd.so\n"
"extension=nd_pdo_mysql.so\n",
},
"binding": {
# php82+ only - see BINDING_MYSQLI_INI_PRE82 for php<82. From php82 the
# binding mysqli.so is mysqlnd-based too, so the edge is real in this
# mode as well.
"mysqli.ini": "; Enable mysqli extension module\n"
"extension=mysqlnd.so\nextension=mysqli.so\n",
"pdo_mysql.ini": "; Enable pdo_mysql extension module\n"
"extension=pdo.so\nextension=pdo_mysql.so\n",
},
}
# Bodies written to php.d (what native/root PHP scans). Identical to the
# php.d.all ones minus the mysqlnd declaration, which MYSQLND_INI_NAME owns.
ACTIVE_INI = {
"native": {
"mysqli.ini": "; Enable mysqli native driver extension\n"
"extension=nd_mysqli.so\n",
"pdo_mysql.ini": "; Enable pdo_mysql native driver extension\n"
"extension=pdo.so\nextension=nd_pdo_mysql.so\n",
},
"binding": {
"mysqli.ini": "; Enable mysqli extension module\n"
"extension=mysqli.so\n",
"pdo_mysql.ini": "; Enable pdo_mysql extension module\n"
"extension=pdo.so\nextension=pdo_mysql.so\n",
},
}
# php<82 ships a plain client-lib mysqli.so that must not load mysqlnd at all,
# in either directory. Nothing else differs by version: pdo_mysql binding never
# declared mysqlnd, and native only exists on php82+ (see NATIVE_SO).
BINDING_MYSQLI_INI_PRE82 = ("; Enable mysqli extension module\n"
"extension=mysqli.so\n")
# ALTEAPLN-12: sole owner of extension=mysqlnd.so for the native php.d scan.
# The 00- prefix makes php.d read it before every nd_* consumer, and it lives
# directly in php.d (like default.ini from alt-phpXY-common) rather than in
# php.d.all, so it is not offered as a selectable extension. Only php82+ has
# the nd_* modules. It is deliberately NOT the Selector's mysqlnd owner - see
# the note above; php.d.all/mysqlnd.ini from alt-phpXY-mysqlnd is.
MYSQLND_INI_NAME = "00-mysqlnd.ini"
MYSQLND_EXT_LINE = "extension=mysqlnd.so"
MYSQLND_INI_BODY = ("; mysqlnd must load before nd_mysqli/nd_pdo_mysql "
"(php.d is scanned alphabetically)\n"
+ MYSQLND_EXT_LINE + "\n")
# native .so that must exist for a given ini before we may switch it to native
NATIVE_SO = {"mysqli.ini": "nd_mysqli.so", "pdo_mysql.ini": "nd_pdo_mysql.so"}
# ALTPHP-2598: the php.d.all bodies alt-phpXY-mysqlnd owns, and the dependency
# each one must declare ahead of its own module.
#
# Nothing rewrites these on upgrade. They are config(noreplace) and their
# packaged bodies have not changed in years, which parks rpm permanently in its
# "old payload == new payload" case: a local edit is kept for good and no
# .rpmnew is ever written. A body stripped down to its own extension= line
# therefore deletes the edge from the Selector/CageFS graph for good - that is
# how ZD 292358 lost mysqli across a 9-server fleet, on versions the packaging
# regression never touched. The alias files above are rewritten unconditionally
# by set_driver_ini(); these are not, so normalize them here.
CHAIN_INI_DEPS = {
"nd_mysqli.ini": ("nd_mysqli.so", ("mysqlnd.so",)),
"nd_pdo_mysql.ini": ("nd_pdo_mysql.so", ("pdo.so", "mysqlnd.so")),
}
def normalize_chain_ini(int_name, int_ver):
"""
ALTPHP-2598: put back dependency declarations missing from the php.d.all
bodies alt-phpXY-mysqlnd owns.
The PHP Selector and the CageFS per-user generator derive the entire load
order from the extension= lines in these bodies, so a dependency that is
not declared here does not exist as far as they are concerned: they emit
nd_mysqli.so ahead of mysqlnd.so and PHP fails the extension with
'undefined symbol: mysqlnd_global_stats'.
Deliberately conservative - this runs unattended from every
alt-phpXY-mysql-meta posttrans:
- only an existing file is repaired. An absent one means the owning
package is not installed, and writing it would declare a module that is
not there, trading one startup error for another.
- a dependency whose .so is missing is skipped, for the same reason.
- a body that does not load its own module is left alone; it has been
disabled on purpose and is not ours to second-guess.
- the declaration is inserted ahead of the module's own line and the rest
of the body is preserved, so local additions survive.
Returns True if any file was rewritten.
"""
if int_name != "php":
return False
root = get_int_files_root_path(int_name, int_ver)
ini_all = os.path.join(root, "etc", "php.d.all")
modules_dir = os.path.join(root, "usr", detect_lib_dir(), "php", "modules")
changed = False
for ini_name in sorted(CHAIN_INI_DEPS):
own_so, deps = CHAIN_INI_DEPS[ini_name]
path = os.path.join(ini_all, ini_name)
if not os.path.isfile(path):
continue
try:
with open(path) as fh:
lines = fh.read().splitlines()
except (IOError, OSError) as e:
logging.error(u"cannot read %s: %s" % (path, e))
continue
# Exact line membership, never a substring test - the substring
# shortcut is precisely what broke lvemanager's resolver in
# ALTEAPLN-53.
own_line = "extension=%s" % own_so
if own_line not in lines:
continue
own_at = lines.index(own_line)
head, tail = lines[:own_at], lines[own_at:]
# A declaration sitting after the module's own line is as useless as a
# missing one, so treat both the same way and re-seat it.
missing = [d for d in deps
if "extension=%s" % d not in head
and os.path.exists(os.path.join(modules_dir, d))]
if not missing:
continue
stale = ["extension=%s" % d for d in missing]
lines = head + stale + [l for l in tail if l not in stale]
try:
with open(path, "w") as fh:
fh.write("\n".join(lines) + "\n")
except (IOError, OSError) as e:
logging.error(u"cannot write %s: %s" % (path, e))
continue
changed = True
logging.info(u"php%s: %s restored %s" %
(int_ver, ini_name, ", ".join(missing)))
return changed
# Duplicate declarations between php.d/default.ini and the php.d files this
# tool writes.
#
# default.ini is the one php.d file admins edit by hand: alt-phpXY used to ship
# it with the whole extension list commented out, and "uncomment the line" is
# how the native/root PHP got its extensions. Since ALTPHP-2242 this tool also
# declares the mysql chain in php.d (00-mysqlnd.ini, mysqli.ini,
# pdo_mysql.ini), so every server where an admin had uncommented one of those
# modules loads it twice and PHP warns 'Module "X" is already loaded' on every
# invocation (ZD 294842).
#
# default.ini belongs to the admin and is NEVER written, not even to comment a
# line out - that is a promise made to customers. It is only read. The php.d
# files this tool owns yield instead: a module default.ini already loads is
# commented out in them, with a marker, and put back as soon as default.ini
# stops loading it.
#
# Order: PHP dlopen()s extensions in scan order and nd_mysqli/nd_pdo_mysql need
# mysqlnd's data symbols at load time. default.ini is read after 00-mysqlnd.ini
# but before mysqli.ini and pdo_mysql.ini, so dropping a later file's copy never
# moves a module later; dropping 00-mysqlnd.ini's copy does, and is only done
# when no mysqlnd consumer is read in between.
DEFAULT_INI_NAME = "default.ini"
# the php.d files set_driver_ini() / create_symlink_to_mysqli_ini() write
MANAGED_PHP_D_INI = (MYSQLND_INI_NAME, "mysqli.ini", "pdo_mysql.ini")
YIELD_MARK = " ; alt-php-mysql-reconfigure: loaded by php.d/" + DEFAULT_INI_NAME
_EXT_LINE_RE = re.compile(r'^\s*extension\s*=\s*"?([^";\s]+)"?\s*(;.*)?$')
def ext_module(line):
"""Module name an active extension= line loads, or None."""
m = _EXT_LINE_RE.match(line)
if not m:
return None
name = os.path.basename(m.group(1))
if name.endswith(".so"):
name = name[:-3]
return name
def module_requires(module, dep):
"""True if `module` needs `dep` loaded before it. Only mysqlnd matters:
nd_mysqli/nd_pdo_mysql resolve its data symbols at dlopen() time. Erring
towards True only ever keeps a declaration."""
if dep == "mysqlnd":
return (module in ("mysqli", "nd_mysqli", "pdo_mysql", "nd_pdo_mysql",
"mysql_xdevapi") or module.startswith("mysqlnd_"))
return False
def _read_lines(path):
with open(path) as fh:
return fh.read().splitlines()
def yielded_line(line):
"""The original extension= line behind a line this tool commented out
because default.ini loads it, or None for any other line."""
s = line.rstrip()
if s.startswith(";") and s.endswith(YIELD_MARK):
return s[1:-len(YIELD_MARK)]
return None
def unyield_body(body):
"""`body` with every yielded line put back - the canonical body."""
out = []
for line in body.splitlines():
orig = yielded_line(line)
out.append(line if orig is None else orig)
return "".join(l + "\n" for l in out)
def _php_d_ini_names(ini_d):
return sorted(n for n in os.listdir(ini_d) if n.endswith(".ini"))
def _file_modules(path):
try:
return [m for m in (ext_module(l) for l in _read_lines(path)) if m]
except (IOError, OSError):
return []
def yield_to_default_ini(int_name, int_ver):
"""
Keep the php.d files this tool writes from loading a module that
php.d/default.ini already loads. default.ini is only read, never written.
Deliberately conservative - this runs unattended from scriptlets:
- lines are commented out with a marker, never deleted, and a marked line
is restored once default.ini no longer loads that module, so the admin
stays in control from default.ini alone.
- 00-mysqlnd.ini keeps mysqlnd when a mysqlnd consumer is read between it
and default.ini's own declaration (the duplicate is then logged).
- php<82 php.d files that are symlinks into php.d.all (the Selector's
graph) are left alone; on php82+ such a symlink is replaced by a file,
never written through.
Returns True if any file was rewritten.
"""
if int_name != "php":
return False
root = get_int_files_root_path(int_name, int_ver)
ini_d = os.path.join(root, "etc", "php.d")
if not os.path.isdir(ini_d):
return False
try:
ver_num = int(int_ver)
except (TypeError, ValueError):
ver_num = 0
default_path = os.path.join(ini_d, DEFAULT_INI_NAME)
default_mods = []
if os.path.isfile(default_path):
default_mods = _file_modules(default_path)
names = _php_d_ini_names(ini_d)
changed = False
for name in MANAGED_PHP_D_INI:
path = os.path.join(ini_d, name)
if not os.path.isfile(path):
continue
if os.path.islink(path) and ver_num < 82:
continue
try:
lines = _read_lines(path)
except (IOError, OSError) as e:
logging.error(u"cannot read %s: %s" % (path, e))
continue
new = []
for line in lines:
orig = yielded_line(line)
base = line if orig is None else orig
mod = ext_module(base)
drop = False
if mod and mod in default_mods:
drop = True
if name < DEFAULT_INI_NAME:
# moving this module later: everything read before
# default.ini's own declaration must manage without it
before = [m for f in names
if name < f < DEFAULT_INI_NAME
for m in _file_modules(os.path.join(ini_d, f))]
before += default_mods[:default_mods.index(mod)]
if any(module_requires(m, mod) for m in before):
drop = False
logging.warning(
u"php%s: %s and %s both load %s; kept in %s, a "
u"consumer is read before %s's copy" %
(int_ver, name, DEFAULT_INI_NAME, mod, name,
DEFAULT_INI_NAME))
new.append(";" + base.strip() + YIELD_MARK if drop else base)
if new == lines:
continue
try:
if os.path.islink(path):
os.remove(path)
with open(path, "w") as fh:
fh.write("\n".join(new) + "\n")
changed = True
logging.info(u"php%s: %s now defers to %s for: %s" % (
int_ver, name, DEFAULT_INI_NAME,
", ".join(ext_module(yielded_line(l)) for l in new
if yielded_line(l)) or "nothing"))
except (IOError, OSError) as e:
logging.error(u"cannot write %s: %s" % (path, e))
return changed
def yield_all_to_default_ini(int_name, int_versions):
"""yield_to_default_ini() for every version; never fails the caller."""
if int_name != "php":
return
for int_ver, int_dir in int_versions:
try:
yield_to_default_ini(int_name, int_ver)
except Exception as e:
logging.error(u"php%s: default.ini dedupe failed: %s" % (int_ver, e))
def ini_bodies(driver, ini_name, ver_num):
"""
ALTEAPLN-53: return (php_d_all_body, php_d_body) for one alias file.
They differ on php82+: the php.d.all copy declares extension=mysqlnd.so so
the Selector can see the dependency, the php.d copy does not so the native
scan does not load mysqlnd twice.
"""
if driver == "binding" and ini_name == "mysqli.ini" and ver_num < 82:
return BINDING_MYSQLI_INI_PRE82, BINDING_MYSQLI_INI_PRE82
return DRIVER_INI[driver][ini_name], ACTIVE_INI[driver][ini_name]
def set_driver_ini(int_name, int_ver, driver):
"""
ALTPHP-2242: switch the active mysql driver for one alt-php version by
rewriting mysqli.ini / pdo_mysql.ini in php.d.all and activating them in
php.d. For "native", only extensions whose nd_*.so is present are switched;
the rest are left as binding. Does not touch the binding .so symlinks.
ALTEAPLN-12: on php82+ also (re)writes php.d/00-mysqlnd.ini, the single owner
of extension=mysqlnd.so, which must be scanned before any nd_* consumer.
Returns True if at least one .ini was written.
"""
if int_name != "php":
logging.error(u"--driver is only supported for alt-php")
return False
if driver not in DRIVER_INI:
logging.error(u"unknown driver '%s' (use native|binding)" % driver)
return False
root = get_int_files_root_path(int_name, int_ver)
ini_all = os.path.join(root, "etc", "php.d.all")
ini_d = os.path.join(root, "etc", "php.d")
modules_dir = os.path.join(root, "usr", detect_lib_dir(), "php", "modules")
try:
ver_num = int(int_ver)
except (TypeError, ValueError):
ver_num = 0
changed = False
failed = False
# ALTEAPLN-12: make sure the single mysqlnd owner exists before anything that
# needs it.
#
# ALTEAPLN-55: decide that from the bodies about to be written, not from the
# version. php.d gets each alias body minus its extension=mysqlnd.so line
# (that is the whole point of the php.d/php.d.all split), so the owner is
# needed exactly when the php.d.all body for the selected driver declares
# mysqlnd - which is every native selection, because nd_mysqli/nd_pdo_mysql
# are mysqlnd-based and ship from php53, and every php82+ binding selection,
# because ALTPHP-1626 made the binding mysqli.so mysqlnd-based too. Only
# php<82 in binding mode has a plain client-lib mysqli.so that must not load
# mysqlnd at all.
#
# Gating the write on `ver_num >= 82` instead left `--driver native` on
# php53-81 with extension=nd_mysqli.so in php.d and nothing loading mysqlnd
# ahead of it, so the native/root PHP failed with 'undefined symbol:
# mysqlnd_global_stats' and 'Cannot load module "pdo_mysql" because required
# module "mysqlnd" is not loaded' - the ALTEAPLN-53 symptom, on the versions
# ALTEAPLN-53 did not cover. Reachable through a bare
# `alt-php-mysql-reconfigure.py --driver native`, which iterates every
# installed version.
mysqlnd_path = os.path.join(ini_d, MYSQLND_INI_NAME)
# Exact line membership, not a substring test - the substring shortcut is
# precisely what broke lvemanager's resolver in ALTEAPLN-53.
needs_mysqlnd_owner = any(
MYSQLND_EXT_LINE in ini_bodies(driver, ini_name, ver_num)[0].splitlines()
for ini_name in DRIVER_INI[driver])
# Never declare a module that is not installed: that would trade one
# startup error for another.
if not os.path.exists(os.path.join(modules_dir, "mysqlnd.so")):
needs_mysqlnd_owner = False
if needs_mysqlnd_owner:
try:
if not os.path.isdir(ini_d):
os.makedirs(ini_d)
write_it = True
if os.path.exists(mysqlnd_path):
with open(mysqlnd_path) as fh:
write_it = unyield_body(fh.read()) != MYSQLND_INI_BODY
if write_it:
with open(mysqlnd_path, "w") as fh:
fh.write(MYSQLND_INI_BODY)
changed = True
except (IOError, OSError) as e:
logging.error(u"cannot write %s: %s" % (mysqlnd_path, e))
failed = True
elif ver_num < 82 and os.path.exists(mysqlnd_path):
# Switching a php<82 back to the binding driver: nothing in php.d loads
# mysqlnd any more, so leave php.d matching the selected driver exactly.
# Only safe below php82, where alt-phpXY-mysql-meta does not package
# this file - on php82+ it is %config(noreplace) and must be left alone.
try:
os.remove(mysqlnd_path)
changed = True
except OSError as e:
logging.error(u"cannot remove %s: %s" % (mysqlnd_path, e))
failed = True
for ini_name in sorted(DRIVER_INI[driver]):
if driver == "native" and not os.path.exists(
os.path.join(modules_dir, NATIVE_SO[ini_name])):
logging.error(u"php%s: %s absent, cannot switch %s to native" %
(int_ver, NATIVE_SO[ini_name], ini_name))
failed = True
continue
all_content, active_content = ini_bodies(driver, ini_name, ver_num)
all_path = os.path.join(ini_all, ini_name)
try:
with open(all_path, "w") as fh:
fh.write(all_content)
except (IOError, OSError) as e:
logging.error(u"cannot write %s: %s" % (all_path, e))
failed = True
continue
# activate in php.d
if not os.path.isdir(ini_d):
logging.error(u"cannot activate %s: %s is missing" % (ini_name, ini_d))
failed = True
continue
dst = os.path.join(ini_d, ini_name)
try:
if os.path.islink(dst) or os.path.exists(dst):
os.remove(dst)
if active_content == all_content:
# bodies agree (php<82): keep the historical symlink, so
# nothing that expects php.d to point into php.d.all changes.
os.symlink(all_path, dst)
else:
# ALTEAPLN-53: php82+ needs two different bodies - the php.d.all
# one carries extension=mysqlnd.so for the Selector, the php.d
# one must not repeat what MYSQLND_INI_NAME already loads. A
# symlink cannot express that, so write a real file.
with open(dst, "w") as fh:
fh.write(active_content)
except (IOError, OSError) as e:
logging.error(u"cannot activate %s: %s" % (dst, e))
failed = True
continue
changed = True
logging.info(u"php%s: %s -> %s driver" % (int_ver, ini_name, driver))
# fully applied only if something changed and nothing failed
return changed and not failed
def main(sys_args):
try:
opts, args = getopt.getopt(sys_args, "p:P:e:d:v",
["php=", "python=", "ea-php=", "driver=",
"verbose"])
except getopt.GetoptError as e:
sys.stderr.write("cannot parse command line arguments: {0}".format(e))
return 1
verbose = False
driver = None
int_versions = []
int_name = "php"
for opt, arg in opts:
if opt in ("-d", "--driver"):
driver = arg
if opt in ("-p", "--php"):
int_name = "php"
int_path = "/opt/alt/php%s" % arg
if check_alt_path_exists(int_path, int_name, arg):
int_versions.append((arg, int_path))
else:
return 1
elif opt in ("-e", "--ea-php"):
int_name = "ea-php"
int_path = "/opt/cpanel/ea-php%s/root/" % arg
if check_alt_path_exists(int_path, int_name, arg):
int_versions.append((arg, int_path))
else:
return 1
elif opt == "--python":
int_name = "python"
int_path = "/opt/alt/python%s" % arg
if check_alt_path_exists(int_path, int_name, arg):
int_versions.append((arg, int_path))
else:
return 1
if opt in ("-v", "--verbose"):
verbose = True
log = configure_logging(verbose)
if int_name == "ea-php":
int_group = int_name
else:
int_group = "alt-%s" % int_name
if not int_versions:
int_versions = find_interpreter_versions()
log.info(u"installed %s versions are\n%s" % (int_group,
"\n".join(["\t %s: %s" % (int_group, i) for i in int_versions])))
# ALTPHP-2242: validate the --driver option first, so a bad command (e.g. a
# --driver typo, or --driver with a non-php interpreter) is still reported
# even on a frozen system instead of silently exiting 0.
if driver:
if int_name != "php":
log.error(u"--driver is only supported for alt-php, not %s" % int_name)
return 1
if driver not in ("native", "binding"):
log.error(u"unknown driver '%s' (use native|binding)" % driver)
return 1
# ALTPHP-2242: honour the reconfiguration freeze (for --driver too) — after
# option validation, before any ini/symlink rewrite below.
if os.path.exists("/opt/alt/alt-php-config/disable"):
log.info(u"skip reconfiguration, because '/opt/alt/alt-php-config/disable' exists")
return 0
# ALTPHP-2598: repair stripped php.d.all bodies before anything reads them.
# Runs for every installed version and independently of --driver: the
# php<82 alt-phpXY-mysql-meta posttrans calls this tool without one, so a
# driver-gated repair would never reach those versions.
if int_name == "php":
for int_ver, int_dir in int_versions:
normalize_chain_ini(int_name, int_ver)
# ALTPHP-2242: explicit driver switch (native nd_* <-> client-lib binding).
driver_ok = True
if driver:
for int_ver, int_dir in int_versions:
if not set_driver_ini(int_name, int_ver, driver):
driver_ok = False
if driver == "native":
# native extensions load directly, no binding .so symlinks needed
yield_all_to_default_ini(int_name, int_versions)
return 0 if driver_ok else 1
# driver == "binding": fall through to refresh the binding .so symlinks
mysql_so_files = get_mysql_so_files()
refresh_ok = True
log.info(u"available SQL so files are\n%s" %
"\n".join(["\t%s (%s-%s)" % i for i in mysql_so_files]))
try:
mysql_path = find_mysql_executable()
if not mysql_path:
log.info(u"cannot find system SQL binary")
for int_ver, int_dir in int_versions:
status = False
for so_name, so_type, so_ver in mysql_so_files:
if reconfigure_mysql(int_ver, so_type, so_ver,
force=False, int_name=int_name):
status = True
break
if not status:
refresh_ok = False
log.debug(u"alt-%s%s reconfiguration is failed" %
(int_name, int_ver))
else:
log.debug(u"system SQL binary path is %s" % mysql_path)
mysql_type, mysql_ver = get_mysql_version(mysql_path)
log.debug(u"system SQL is %s-%s" % (mysql_type, mysql_ver))
# check if we have .so for the system SQL version
mysql_so_exists = False
for so_name, so_type, so_ver in mysql_so_files:
if so_type == mysql_type and so_ver == mysql_ver:
mysql_so_exists = True
break
# reconfigure alt-php symlinks
for int_ver, int_dir in int_versions:
# system SQL was correctly detected and we found .so for it -
# reconfigure alt-php to use it instead of previous
# configuration
if (mysql_so_exists or is_bare_plesk()) and \
reconfigure_mysql(int_ver, mysql_type, mysql_ver,
force=True, int_name=int_name):
ini_src_path = "%s/etc/php.d.all" % get_int_files_root_path(int_name, int_ver)
ini_dst_path = "%s/etc/php.d" % get_int_files_root_path(int_name, int_ver)
if int_name == "php":
create_symlink_to_mysqli_ini(ini_src_path, ini_dst_path, int_ver)
continue
# we are unable to detect system SQL or it's .so is missing -
# reconfigure alt-php to use .so that we have available, but
# only if current configuration is broken
status = False
for so_name, so_type, so_ver in mysql_so_files:
if reconfigure_mysql(int_ver, so_type, so_ver, force=False,
int_name=int_name):
status = True
ini_src_path = "%s/etc/php.d.all" % get_int_files_root_path(int_name, int_ver)
ini_dst_path = "%s/etc/php.d" % get_int_files_root_path(int_name, int_ver)
if int_name == "php":
create_symlink_to_mysqli_ini(ini_src_path, ini_dst_path, int_ver)
break
if not status:
refresh_ok = False
log.debug(u"alt-%s%s reconfiguration is failed" %
(int_name, int_ver))
except Exception as e:
log.error(u"cannot reconfigure alt-%s SQL bindings: %s. "
u"Traceback:\n%s" % (int_name, e,
traceback.format_exc()))
return 1
# after every php.d write above, so it sees the final alias files
yield_all_to_default_ini(int_name, int_versions)
# ALTPHP-2242: propagate --driver binding failures (ini write/activation in
# set_driver_ini, or the binding .so symlink refresh above). The plain
# no-driver run keeps its historical success exit code.
if driver == "binding":
return 0 if (driver_ok and refresh_ok) else 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))