????
Current Path : /usr/local/ssl/bin/ |
Current File : //usr/local/ssl/bin/alt-mysql-reconfigure |
#!/usr/bin/env python # -*- mode:python; coding:utf-8; -*- # author: Eugene Zamriy <ezamriy@cloudlinux.com>, # Sergey Fokin <sfokin@cloudlinux.com> # created: 29.07.2015 15:46 # edited: Sergey Fokin 17.10.2024 12:23 # 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"} 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 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): """ Creates or updates a symbolic link to mysqli.ini. @type source_dir: str @param source_dir: Directory containing the actual mysqli.ini file. @type target_dir: str @param target_dir: Directory where the symlink will be created or updated. @rtype: bool @return: True if symlink is created or updated successfully, False otherwise. """ source_path = os.path.join(source_dir, 'mysqli.ini') target_path = os.path.join(target_dir, 'mysqli.ini') 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): 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 = re.search("mysql\s+Ver\s+(.*?Distrib\s+)?((\d+)\.(\d+)\S*?)(,)?" "\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("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("percona", line, re.IGNORECASE): return "percona", mysql_ver maria_version = re.search("^(10\.[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("5\.5.*?-MariaDB", line, re.IGNORECASE): return "mariadb", "5.5" if re.search("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(): proc = subprocess.Popen(["dpkg-architecture", "-qDEB_HOST_MULTIARCH"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) out, _ = proc.communicate() return "lib/{0}".format(out.strip()) 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('_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 ------- """ if int_name == "ea-php": return ["mysql.so", "mysqli.so", "pdo_mysql.so"] elif int_name == "php": 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"] 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 in ("10.2", "10.3", "10.4", "10.5", "10.6", "10.11"): 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/%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("^.*?=>\s*(\S*?(libmysqlclient|" "libmariadb|" "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 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 == "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.error(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 def main(sys_args): try: opts, args = getopt.getopt(sys_args, "p:P:e:v", ["php=", "python=", "ea-php=", "verbose"]) except getopt.GetoptError as e: sys.stderr.write("cannot parse command line arguments: {0}".format(e)) return 1 verbose = False int_versions = [] int_name = "php" for opt, arg in opts: 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]))) mysql_so_files = get_mysql_so_files() log.info(u"available SQL so files are\n%s" % "\n".join(["\t%s (%s-%s)" % i for i in mysql_so_files])) # skip reconfigure magick if file exists if os.path.exists("/opt/alt/alt-php-config/disable"): log.info(u"skip reconfiguration, because '/opt/alt/alt-php-config/disable' exists") return True 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: log.error(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 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) 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) break if not status: log.error(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 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))