5tarl0rd
Current Path : /bin/ |
|
Current File : //bin/alt-php-mysql-reconfigure.py |
#!/usr/bin/env python
# -*- mode:python; coding:utf-8; -*-
# author: Eugene Zamriy <ezamriy@cloudlinux.com>
# created: 29.07.2015 15:46
# 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 = 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"}
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()
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 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 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
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"
break
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)
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]
# 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)
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 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":
return os.path.join(int_path, lib_dir, "python%s" % int_dot_ver,
"site-packages", 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 = VER_PATTERNS.values()
possible_versions.extend(["10", "10.0", "10.1", "10.2", "10.3", "10.4"])
mysql_type, mysql_ver = get_mysql_version(mysql)
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(ur"(\S*libmysqlclient\.so\.15\.\S*)")
elif mysql_ver == "5.1":
search_pattern = re.compile(ur"(\S*libmysqlclient\.so\.16\.\S*)")
elif mysql_ver in ("5.5", "10", "10.0", "10.1"):
search_pattern = re.compile(ur"(\S*libmysqlclient\.so\.18\.0\.\S*)")
elif mysql_ver == "5.6":
search_pattern = re.compile(ur"(\S*libmysqlclient\.so\.18\.1\.\S*)")
elif mysql_ver == "5.7":
search_pattern = re.compile(ur"(\S*libmysqlclient\.so\.20\.\S*)")
elif mysql_ver == "8.0":
search_pattern = re.compile(ur"(\S*libmysqlclient\.so\.21\.\S*)")
elif mysql_ver in ("10.2", "10.3", "10.4"):
search_pattern = re.compile(ur"(\S*libmariadb\.so\.3\.\S*)")
else:
raise Exception(u"Cannot match MySQL library to any version")
if mysql_type == "percona":
search_path = "/%s" % detect_lib_dir()
else:
search_path = "/usr/%s/" % detect_lib_dir()
if mysql_type == "mariadb" or (mysql_type == "mysql" and Decimal(mysql_ver) >= 8.0):
if os.path.exists(search_path + "mysql"):
search_path += "mysql"
elif os.path.exists(search_path + "mariadb"):
search_path += "mariadb"
else:
raise Exception("Detected %s but path for libraries is not found" % mysql_type)
files = os.listdir(search_path)
for one_file in files:
if search_pattern.match(one_file):
return (os.path.join(search_path,
search_pattern.match(one_file).string),
mysql_type, mysql_ver)
def get_mysql_so_files():
proc = subprocess.Popen(["/sbin/ldconfig", "-p"], stdout=subprocess.PIPE)
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 (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
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):
print >> sys.stderr, u"unknown %s version %s" % (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, e:
print >> sys.stderr, \
u"cannot parse command line arguments: %s" % unicode(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]))
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):
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
break
if not status:
log.error(u"alt-%s%s reconfiguration is failed" %
(int_name, int_ver))
except Exception, e:
log.error(u"cannot reconfigure alt-%s SQL bindings: %s. "
u"Traceback:\n%s" % (int_name, unicode(e),
traceback.format_exc()))
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
5tarL0rd By