Aurora
Adminer
Auto Root
WP Admin
cPanel Reset
Anti Backdoor
Root
usr
bin
Upload
New Folder
New File
Name
Size
Permissions
Actions
..
-
-
-
Upload File
Select File
New Folder
Folder Name
New File
File Name
Add WordPress Admin
Database Host
Database Name
Database User
Database Password
Admin Username
Admin Password
cPanel Password Reset
Email Address
Edit: alt-mysql-reconfigure
#!/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: if not os.path.islink(target_path) and 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"} 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 = 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-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 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 # 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:]))