Mini Shell

Direktori : /usr/share/lve/dbgovernor/modules/
Upload File :
Current File : //usr/share/lve/dbgovernor/modules/base.py

# coding:utf-8

# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
#
"""
This module contains base class for managing governor on all supported
control panels
"""
import grp
import os
import pwd
import re
import shutil
import stat
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime

sys.path.append("../")

from utilities import (
    IS_UBUNTU, RPM_TEMP_PATH, add_line, bcolors, check_file,
    check_mysqld_is_alive, cl8_module_enable, confirm_packages_installation,
    correct_mysqld_service_for_cl7,
    correct_remove_notowned_mysql_service_names_cl7,
    correct_remove_notowned_mysql_service_names_not_symlynks_cl7,
    create_mysqld_link, debug_log, download_packages, exec_command,
    exec_command_out, force_update_cagefs, get_cl_num, get_mysql_log_file,
    get_section_from_all_cnfs, grep, install_deb_packages, install_packages,
    is_package_installed, makedir_recursive, mycnf_writable, mysql_version,
    patch_governor_config, read_config_file, read_file, remove_packages,
    rewrite_file, service, show_new_packages_info, touch,
    wizard_install_confirm, write_file)


class InstallManager:
    """
    Base class with standard methods for any CP
    """
    # installation path
    SOURCE = "/usr/share/lve/dbgovernor/"
    # file with new version for install
    NEW_VERSION_FILE = "/usr/share/lve/dbgovernor/mysql.type"
    # file with cached installed version before install
    CACHE_VERSION_FILE = "/usr/share/lve/dbgovernor/mysql.type.installed"
    # file with cached CL8 module stream before install
    CACHE_MODULE_FILE = "/usr/share/lve/dbgovernor/cl8_module.saved"
    HISTORY_FOLDER = "/usr/share/lve/dbgovernor/history"
    REPO_NAMES = {
        "mysql51": "mysql-5.1",
        "mysql55": "mysql-5.5",
        "mysql56": "mysql-5.6",
        "mysql57": "mysql-5.7",
        "mysql80": "mysql-8.0",
        "mariadb55": "mariadb-5.5",
        "mariadb100": "mariadb-10.0",
        "mariadb101": "mariadb-10.1",
        "mariadb102": "mariadb-10.2",
        "mariadb103": "mariadb-10.3",
        "mariadb104": "mariadb-10.4",
        "mariadb105": "mariadb-10.5",
        "mariadb106": "mariadb-10.6",
        "mariadb1011": "mariadb-10.11",
        "percona56": "percona-5.6"
    }
    MODULE_STREAMS = {
        "mysql55": "mysql:cl-MySQL55",
        "mysql56": "mysql:cl-MySQL56",
        "mysql57": "mysql:cl-MySQL57",
        "mysql80": "mysql:cl-MySQL80",
        "mariadb55": "mariadb:cl-MariaDB55",
        "mariadb100": "mariadb:cl-MariaDB100",
        "mariadb101": "mariadb:cl-MariaDB101",
        "mariadb102": "mariadb:cl-MariaDB102",
        "mariadb103": "mariadb:cl-MariaDB103",
        "mariadb104": "mariadb:cl-MariaDB104",
        "mariadb105": "mariadb:cl-MariaDB105",
        "mariadb106": "mariadb:cl-MariaDB106",
        "mariadb1011": "mariadb:cl-MariaDB1011",
        "percona56": "percona:cl-Percona56",
        "auto": "mysql:8.0"
    }
    ALL_PACKAGES_NEW_NOT_DOWNLOADED = False
    ALL_PACKAGES_OLD_NOT_DOWNLOADED = False
    DISABLED = False
    ROLLBACK = False
    MYSQLUSER = ''
    MYSQLPASSWORD = ''
    MY_CNF_BACKUP_PATH = "/etc/my.cnf.govprev"
    MY_CNF_D_BACKUP_PATH = "/etc/my.cnf.d.govprev"

    @staticmethod
    def factory(cp_name):
        """
        Get object instance for specific cp
        """
        if "cPanel" == cp_name:
            from .cpanel import cPanelManager
            return cPanelManager(cp_name)
        elif "DirectAdmin" == cp_name:
            from .da import DirectAdminManager
            return DirectAdminManager(cp_name)
        elif "Plesk" == cp_name:
            from .plesk import PleskManager
            return PleskManager(cp_name)
        elif "ISPManager" == cp_name:
            from .ispmanager import ISPMManager
            return ISPMManager(cp_name)
        elif "InterWorx" == cp_name:
            from .iworx import IWorxManager
            return IWorxManager(cp_name)
        else:
            return InstallManager(cp_name)

    def __init__(self, cp_name):
        self.cl_version = get_cl_num()
        self.cp_name = cp_name
        self.prev_version = None
        self._old_packages = []
        self._new_packages = []

        # In case of custom database my.cnf file can be located in some nonstandard path.
        # That's why we try to get it from os environ
        self.my_cnf_path = '/etc/my.cnf'
        self.VAR_LIB_MYSQL = '/var/lib/mysql'
        self.my_cnf_datadir = get_section_from_all_cnfs('datadir')

    def _backup_my_cnf_d(self, source_dir='/etc/my.cnf.d'):
        """
        Create a copy of configs in my.cnf.d to restore them after installation of CL-patched mysql package.

        mysqlgovernor install should work like transaction,
        so we need only one backup from current mysql installation
        """
        if os.path.exists(source_dir):
            shutil.rmtree(self.MY_CNF_D_BACKUP_PATH, ignore_errors=True)
            shutil.copytree(source_dir, self.MY_CNF_D_BACKUP_PATH)

    def _restore_all_my_cnf(self, my_cnf_src=MY_CNF_BACKUP_PATH):
        """
        Move backups of /etc/my.cnf and /etc/my.cnf.d back to original locations.
        """
        my_cnf_dest = '/etc/my.cnf'
        my_cnf_d_dest = '/etc/my.cnf.d'
        shutil.copy2(my_cnf_src, my_cnf_dest)
        # copy all files from backup dir to original destination, overwrite existing files
        # can't use shutil.copytree(..., dirs_exist_ok=True) for python < 3.8
        for src_dirpath, _, files in os.walk(self.MY_CNF_D_BACKUP_PATH):
            dest_dirpath = src_dirpath.replace(self.MY_CNF_D_BACKUP_PATH, my_cnf_d_dest, 1)
            if not os.path.exists(dest_dirpath):
                os.makedirs(dest_dirpath)
            for file in files:
                src_path = os.path.join(src_dirpath, file)
                dest_path = os.path.join(dest_dirpath, file)
                shutil.copy2(src_path, dest_path)

    def _cleanup_all_my_cnf(self, _):
        """
        Delete internal backups of /etc/my.cnf and /etc/my.cnf.d
        """
        try:
            if os.path.exists(self.MY_CNF_BACKUP_PATH):
                os.unlink(self.MY_CNF_BACKUP_PATH)
            if os.path.exists(self.MY_CNF_D_BACKUP_PATH):
                shutil.rmtree(self.MY_CNF_D_BACKUP_PATH)
        except PermissionError as e:
            print(f'Unable to delete internal /etc/my.cnf* backups:\n{e}')

    def my_cnf_manager(self, action, old_path=None):
        """
        Allows to manage known manipulations with /etc/my.cnf file
        :param action: action to perform
        :param old_path: path to my.cnf if needed
        """
        # fix for packages without /etc/my.cnf file
        if action == 'touch':
            if mycnf_writable():
                touch("/etc/my.cnf")
            return

        actions = {
            'backup': lambda x: shutil.copy2(x, self.MY_CNF_BACKUP_PATH),
            'restore': self._restore_all_my_cnf,
            'restore_old': lambda x: shutil.copy(x, "/etc/my.cnf"),
            'restore_rpmsave': lambda x: shutil.copy2(x, "/etc/my.cnf"),
            'cleanup': self._cleanup_all_my_cnf,
            'backup_old': '',
            'backup_my_cnf_d': self._backup_my_cnf_d,
        }

        if action not in actions.keys():
            raise RuntimeError(f'Cannot manage /etc/my.cnf: unknown action {action}')

        if action == 'backup_my_cnf_d':
            my_cnf_d_path = '/etc/my.cnf.d'
            if os.path.exists(my_cnf_d_path):
                actions.get(action)(my_cnf_d_path)
                return

        if action == 'backup':
            working_path = "/etc/my.cnf"
        elif action == 'restore_old':
            working_path = f"{old_path}/my.cnf"
        else:
            working_path = self.MY_CNF_BACKUP_PATH

        if os.path.exists(working_path):
            try:
                if action == 'backup_old':
                    shutil.move(working_path, "%s/my.cnf" % old_path)
                else:
                    actions.get(action)(working_path)
            except PermissionError as e:
                print(f'Unable to perform actions on the my.cnf : {e}')

    def my_cnf_inspect(self):
        """
        Fix nonexistent paths to log-error and pid-file
        """
        track = {
            'files': ('log-error', ),
            'paths': ('pid-file', )
        }
        default_log = f'{self.my_cnf_datadir}/mysqld.error.log'
        default_pid = f'{self.my_cnf_datadir}/mysqld.pid'

        conf = read_config_file('/etc/my.cnf')
        # try to find non-existent paths, defined in /etc/my.cnf
        for s in conf.sections():
            for opt, val in conf.items(s):
                if opt in track['files']:
                    # inspect whole path
                    if not os.path.exists(val):
                        print('NO LOG for {opt} --> {v}'.format(opt=opt, v=val))
                        conf.set(s, opt, default_log)
                elif opt in track['paths']:
                    # inspect dir path
                    if not os.path.exists(os.path.dirname(val)):
                        print('NO PATH for {opt} --> {v}'.format(opt=opt, v=val))
                        conf.set(s, opt, default_pid)

        if self._get_new_version() == 'mysql80':
            # for mysql80 set old authentication plugin as default one
            # in order to prevent php connection errors
            # MYSQLG-297, MySQLG-301
            if not conf.has_section('mysqld'):
                conf.add_section('mysqld')
            conf.set('mysqld', 'default-authentication-plugin', 'mysql_native_password')

        if mycnf_writable() and conf.sections():
            with open('/etc/my.cnf', 'w') as configfile:
                conf.write(configfile)

    def remove_current_packages(self):
        """
        Delete current installed packages
        """
        # stop mysql service
        self._mysqlservice("stop")
        # disable service for CL7 to eliminate broken symlinks after removing
        if self.cl_version >= 7:
            self._mysqlservice("disable")

        # remove current mysql packages
        remove_packages(self._old_packages)

    def install(self, beta, no_confirm, wizard_mode):
        """
        Install stable or beta packages
        @param `beta` bool: install beta or production
        @param `path` str: path to packages for install
        """
        if not self.cl_version:
            print("Unknown system type. Installation aborted")
            sys.exit(1)

        self._before_install()

        # remember installed mysql version
        self.prev_version = self._check_mysql_version()

        # first download packages for current and new mysql versions
        self._load_packages(beta)

        # save current installed mysql version
        self._save_previous_version()

        if self.ALL_PACKAGES_NEW_NOT_DOWNLOADED:
            self.print_warning_about_not_complete_of_newpkg_saving()
            return False

        if self.ALL_PACKAGES_OLD_NOT_DOWNLOADED:
            self.print_warning_about_not_complete_of_pkg_saving()

        new_version = show_new_packages_info("new")

        if wizard_mode:
            # wizard mode has its own confirmation logic
            if not wizard_install_confirm(new_version, self.prev_version):
                self.cl8_enable_cached()
                sys.exit(3)
        elif not confirm_packages_installation(new_version,
                                               self.prev_version,
                                               no_confirm):
            self.DISABLED = True
            self.cl8_enable_cached()
            return False

        self.my_cnf_manager('backup_my_cnf_d')
        self.my_cnf_manager('backup')

        create_mysqld_link("mysqld", "mysql")
        create_mysqld_link("mysql", "mysqld")

        # first remove installed mysql packages
        self.remove_current_packages()

        correct_remove_notowned_mysql_service_names_cl7()
        correct_remove_notowned_mysql_service_names_not_symlynks_cl7()

        # restore my.cnf, because before removing packages, we make backup of
        # /etc/my.cnf and /etc/my.cnf.d/* to *.govprev extension
        self.my_cnf_manager('restore_rpmsave')

        self.set_fs_suid_dumpable()
        self._check_leave_pid()

        self._before_install_new_packages()

        # don`t know for what this
        self._kill_mysql()

        try:
            # new db version which will be installing
            if not install_packages("new", beta):
                # if not install new packages - don`t do next actions
                return False
        except RuntimeError:
            return False

        create_mysqld_link("mysqld", "mysql")
        create_mysqld_link("mysql", "mysqld")

        correct_mysqld_service_for_cl7(self._get_result_mysql_version(None))

        # fix for packages without /etc/my.cnf file
        # if not os.path.exists("/etc/my.cnf"):
        #     touch("/etc/my.cnf")
        self.my_cnf_manager('touch')

        # check if log MySQL's log file exists and correct perms
        # in other case MySQL will not starts
        log_file = get_mysql_log_file()
        makedir_recursive(log_file)
        touch(log_file)
        log_owner_name = pwd.getpwuid(os.stat(log_file).st_uid)[0]
        log_owner_grp = grp.getgrgid(os.stat(log_file).st_gid)[0]
        if log_owner_name != "mysql" or log_owner_grp != "mysql":
            target_uid = pwd.getpwnam("mysql").pw_uid
            target_gid = grp.getgrnam("mysql").gr_gid
            os.chown(log_file, target_uid, target_gid)

        self.my_cnf_inspect()

        version = self._get_new_version()
        if version.startswith("mariadb") or version == "auto" \
                and self.cl_version >= 7:
            self._enable_mariadb()

        if version.startswith("mysql") \
                and self.cl_version >= 7:
            self._enable_mysql()

        if version.startswith("percona") \
                and self.cl_version >= 7:
            self._enable_percona()

        if not os.path.exists(self.VAR_LIB_MYSQL):
            os.makedirs(self.VAR_LIB_MYSQL)
            shutil.chown(self.VAR_LIB_MYSQL, user='mysql', group='mysql')

        self._mysqlservice("restart")

        print("Giving mysqld a few seconds to start up...")
        time.sleep(5)

        if is_package_installed("governor-mysql"):
            service("restart", "db_governor")
            print(bcolors.ok("DB-Governor installed/updated..."))

        self._after_install_new_packages()

        self._ld_fix()

        self._after_install()

        return True

    def install_rollback(self, beta):
        """
        Rollback installed version
        """
        if self.ALL_PACKAGES_OLD_NOT_DOWNLOADED:
            self.print_warning_about_not_complete_of_pkg_saving()
            print(bcolors.fail("Rollback disabled"))
            return False

        # to enable previously enabled mysql module
        self.cl8_enable_cached()

        # self._before_install_new_packages()
        self._mysqlservice("stop")

        installed_packages = self._load_current_packages(False)

        # remove new installed packages
        remove_packages(installed_packages)

        if os.path.exists("/etc/yum.repos.d/cl-mysql.repo.bak"):
            shutil.move("/etc/yum.repos.d/cl-mysql.repo.bak",
                        "/etc/yum.repos.d/cl-mysql.repo")

        # install deleted packages with triggers etc
        if self._custom_rpm_installer("", True) == "yes":
            install_packages("old", beta, self._custom_rpm_installer)
        else:
            install_packages("old", beta)

        # restore previous configs for affected mysql packages
        self.my_cnf_manager('restore')

        self._mysqlservice("restart")

        self._after_install_rollback()

        self.ROLLBACK = True

        return True

    def install_from_history(self, timestamp):
        """
        Install packages from history by timestamp value
        """
        try:
            timestamp = int(timestamp)
        except (TypeError, ValueError):
            print("Invalid parameters", file=sys.stderr)
            return False

        history_path = os.path.join(self.HISTORY_FOLDER, "old.%s" % timestamp)
        if not os.path.isdir(history_path):
            print("No packages for timestamp: %s" % timestamp, file=sys.stderr)
            return False

        self._mysqlservice("stop")

        # remove current installed packages
        installed_packages = self._load_current_packages(False)
        remove_packages(installed_packages)

        # install history packages
        install_packages(history_path, False, abs_path=True)

        # restore old config file
        # old_cnf = "%s/my.cnf" % history_path
        # if os.path.exists(old_cnf):
        #     shutil.copy(old_cnf, "/etc/my.cnf")
        self.my_cnf_manager('restore_old', history_path)

        self._mysqlservice("restart")

    def delete(self):
        """
        Delete governor packages
        """
        # first check config file
        check_file("/etc/my.cnf")

        # save current installed mysql version
        # self._save_previous_version()

        # get list of installed packages
        installed_packages = self._load_current_packages(download=False)

        installed_db_version = self._check_mysql_version().get('full')

        if not installed_db_version:
            print(bcolors.info("Current installed database couldn't be found. Exiting..."))
            exit(1)

        previous_db_version = self._get_previous_version()

        if previous_db_version == 'auto':
            previous_db_version = 'mysql80'

        print('installed_db_version ->', installed_db_version)
        print('previous_db_version ->', previous_db_version)

        if installed_db_version > previous_db_version:
            print("Not supported to downgrade database. Exiting...")
            exit(1)

        # remove repo file
        if os.path.exists("/etc/yum.repos.d/cl-mysql.repo"):
            shutil.move("/etc/yum.repos.d/cl-mysql.repo",
                        "/etc/yum.repos.d/cl-mysql.repo.bak")

        # remove cron file
        if os.path.exists("/etc/cron.d/dbgovernor-usermap-cron"):
            shutil.move("/etc/cron.d/dbgovernor-usermap-cron",
                        "/etc/cron.d/dbgovernor-usermap-cron.bak")

        # backup my.cnf* files for restoration in case of uninstall failure
        self.my_cnf_manager('backup')

        # run trigger before governor uninstal
        self._before_delete()

        # run uninstall action
        self._delete(installed_packages)

        # run trigger after governor uninstall
        self._after_delete()

    def cl8_enable_cached(self):
        """
        Enable saved module stream, e.g. previously/initially enabled one (CL8 method only),
        or clean all mysql|mariadb|percona modules settings
        """
        if self.cl_version >= 8:
            if os.path.exists(self.CACHE_MODULE_FILE):
                with open(self.CACHE_MODULE_FILE) as module_file:
                    module = module_file.read()
                cl8_module_enable(module)
                print('Cached module: %s' % module)
            else:
                exec_command(
                    'dnf module disable -y mysql && dnf module disable -y mariadb && dnf module disable -y percona',
                    True, silent=True)

    def cl8_save_current(self):
        """
        Save currently enabled module stream (CL8 method only)
        """
        if self.cl_version >= 8:
            enabled_modules = exec_command('dnf module list --enabled --quiet | grep -iE "^mysql|mariadb|percona"',
                                           silent=True)
            try:
                current_module = ':'.join([l for l in enabled_modules[0].split(' ') if l][:2])
                print('Saving current enabled module: %s' % current_module)
                debug_log(exec_command('dnf module list --enabled', as_string=True))
                with open(self.CACHE_MODULE_FILE, 'w') as module_file:
                    module_file.write(current_module)
            except IndexError:
                pass

    def show_packages_history(self):
        """
        Show early downloaded packages
        """
        h = self.HISTORY_FOLDER

        for path in sorted(os.listdir(h)):
            full_path = os.path.join(h, path)
            if not os.path.isdir(full_path) or path.count(".") != 1:
                continue

            _, timestamp = path.split(".")
            try:
                timestamp = int(timestamp)
            except (ValueError, TypeError):
                continue

            date = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M")
            print("DATE: %s, TS: %s" % (date, timestamp))
            for name in sorted(os.listdir(full_path)):
                print("    %s" % name)

    def clear_history_folder(self):
        """
        Remove all downloaded packages
        """
        if os.path.isdir(self.HISTORY_FOLDER):
            shutil.rmtree(self.HISTORY_FOLDER)
            os.mkdir(self.HISTORY_FOLDER, 0o755)
            os.chmod(self.HISTORY_FOLDER, 0o755)

    def cleanup(self):
        """
        Cleanup downloaded packages and remove backup repo file
        """
        # to enable previously enabled mysql module
        self.cl8_enable_cached()

        tmp_path = f"{RPM_TEMP_PATH}/old"
        if os.path.isdir(tmp_path):
            # first move previous downloaded packages to history folder
            history_path = os.path.join(self.HISTORY_FOLDER, "old.%s" %
                                        int(time.time()))
            shutil.move(tmp_path, history_path)
            # if os.path.exists("/etc/my.cnf.govprev"):
            #     shutil.move("/etc/my.cnf.govprev", "%s/my.cnf" % history_path)
            self.my_cnf_manager('backup_old', history_path)

        if os.path.exists(RPM_TEMP_PATH):
            shutil.rmtree(RPM_TEMP_PATH)

        if os.path.exists("/etc/yum.repos.d/cl-mysql.repo.bak"):
            os.unlink("/etc/yum.repos.d/cl-mysql.repo.bak")

        self.my_cnf_manager('cleanup')

        if os.path.exists("/etc/cron.d/dbgovernor-usermap-cron.bak"):
            os.unlink("/etc/cron.d/dbgovernor-usermap-cron.bak")

    def save_installed_version(self):
        """
        Save installed version number in file
        """
        self._save_previous_version()

    def update_user_map_file(self):
        """
        Update user mapping file.
        By default - empty
        """
        pass

    def set_bad_lve_container(self):
        """
        Function with some manipulations with lvectl and limits for container
        No more need in changing. Lets user decide by himself the value of the limits
        """
        return

    def install_mysql_beta_testing_hooks(self):
        """
        cPanel specific action
        """
        # self.set_fs_suid_dumpable()
        print("No need in fix")

    def update_mysql_hooks(self):
        """
        cPanel specific action
        """
        print("No need in update")

    def fix_mysqld_service(self):
        """
        DA specific action
        """
        print("No need in fix")

    def set_mysql_version(self, version):
        """
        Set new mysql version for next install
        """
        # check available versions
        versions = ["auto"] + list(self.REPO_NAMES.keys())
        if version not in versions:
            print("Invalid mysql version.", file=sys.stderr)
            print("Available versions: %s" % ", ".join(versions), file=sys.stderr)
            sys.exit(1)

        write_file(self.NEW_VERSION_FILE, version)

    @staticmethod
    def set_fs_suid_dumpable():
        """
        Run this code in spec file
        """
        def check_io_perms():
            """
            Returns True if io is readable and False otherwise.
            Readable io means that fs.suid_dumpable could be 0, otherwise it should be 1 for governor's correct work
            """
            mode = os.stat('/proc/{0}/task/{0}/io'.format(os.getpid())).st_mode
            return stat.S_IMODE(mode) == 0o444
        suid_dumpable_state = 'fs.suid_dumpable={0:d}'.format(not check_io_perms())
        print("Setting FS suid_dumpable for the governor to work correctly ({0})".format(suid_dumpable_state))
        exec_command_out("sysctl -w {0}".format(suid_dumpable_state))
        if os.path.exists("/etc/sysctl.conf"):
            if not grep("/etc/sysctl.conf", 'fs.suid_dumpable='):
                print("Adding suid_dumpable instruction to /etc/sysctl.conf for the governor to work correctly")
                shutil.copy("/etc/sysctl.conf", "/etc/sysctl.conf.bak")
                add_line("/etc/sysctl.conf", suid_dumpable_state)
            else:
                print("Rewriting suid_dumpable instruction in /etc/sysctl.conf")
                with open("/etc/sysctl.conf", 'r+') as f:
                    rewrite_file(f, re.sub(r'fs.suid_dumpable=\d{1}', suid_dumpable_state, f.read()))
        else:
            print("Creating /etc/sysctl.conf for the governor to work correctly")
            add_line("/etc/sysctl.conf", suid_dumpable_state)

    def _load_packages(self, beta):
        """
        Detect current mysql packages.
        Detect new mysql packages.
        Download rpm files of current and new mysql packages.
        """
        self._old_packages = self._load_current_packages()
        self._new_packages = self._load_new_packages(beta)

    @staticmethod
    def print_warning_about_not_complete_of_pkg_saving():
        """
        Display warning in case of failed download of old packages
        """
        print(bcolors.fail(
            """Restoration of MySQL packages will not be completed because not \
all old packages were downloaded.\nCheck if all related repositories \
are enabled and try again\nIf something went wrong during \
or after installation process, execute \
/usr/share/lve/dbgovernor/mysqlgovernor --delete \
for the native restoration procedure of MySQL packages"""))

    @staticmethod
    def print_warning_about_not_complete_of_newpkg_saving():
        """
        Display warning in case of failed download of new packages
        """
        print(bcolors.fail("Installation of MySQL packages will not be completed " \
                           "because not all of the new packages have been downloaded"))

    def _load_current_packages(self, download=True, folder="old"):
        """
        here we download current installed packages
        @param `download` bool: download rpm files or
                                only return list of installed packages
        """
        if download:
            print(bcolors.info("Start downloading current installed packages"))
        else:
            print(bcolors.info("Get the list of current installed packages"))

        PATTERNS = ["cl-mysql", "cl-mariadb", "cl-percona", "mysql", "mariadb",
                    "compat-mysql5", "Percona"]

        mysqld_path = exec_command("which mysqld", True, silent=True)
        pkg_name = False
        if mysqld_path:
            # print "No mysql presents on system"
            # return None

            # server package name
            # pkg_name = exec_command("""rpm -qf --qf "%%{name}
            # %%{version}\n" %s """ % mysqld_path, True, silent=True)
            check_if_mysql_installed = exec_command("""rpm -qf %s """ %
                                                    mysqld_path, True,
                                                    silent=True,
                                                    return_code=True)
            if check_if_mysql_installed == "no":
                print("No mysql packages installed, " \
                      "but a mysqld file is present on system")
                pkg_name = None
            else:
                pkg_name = exec_command("""rpm -qf %s """ % mysqld_path, True,
                                        silent=True)

        # grep cl-MySQL packages in installed list
        # packages = exec_command("""rpm -qa --qf "%%{name}
        # %%{version}\n"|grep -iE "^(%s)" """ % "|".join(PATTERNS), silent=True)
        packages = exec_command("""rpm -qa|grep -iE "^(%s)" """ %
                                "|".join(PATTERNS), silent=True)
        # match pattern to exclude:
        # - mysql-community release package
        # - mysqld_exporter package
        # - MySQL-python package
        # from the list of packages to download
        pattern = r'MySQL-python|mysql(d_exporter|\d+-community-release)|mysql-common'
        packages = [x for x in packages if not re.match(pattern, x)]

        if not packages:
            print("No installed DB packages found")
            return False

        if pkg_name:
            found = False
            for pkg in packages:
                if pkg.startswith(pkg_name):
                    found = True
                    break

            if not found:
                packages.append(pkg_name)
                # print "Can`t find package with mysqld file"

        # self._old_packages = packages
        if download:
            # arch = ".x86_64" if os.uname()[-1] == "x86_64" else ""
            # download_pkgs = ["%s%s" % (x.split(" ")[0], arch)
            # for x in packages]
            IS_CL_MYSQL = False
            for package_item in packages:
                if "server" in package_item and package_item[:3] == "cl-":
                    IS_CL_MYSQL = True

            if IS_CL_MYSQL is True:
                if not download_packages(packages, folder, True):
                    self.ALL_PACKAGES_OLD_NOT_DOWNLOADED = True
            else:
                if not download_packages(packages, folder, True,
                                         self._custom_download_of_rpm):
                    print(bcolors.info("Trying to load custom packages from yum"))
                    if not download_packages(packages, folder, True):
                        self.ALL_PACKAGES_OLD_NOT_DOWNLOADED = True

        return packages

    def _get_result_mysql_version(self, sql_version=None):
        """
        Get MySQL version will be installed according to auto or mysql.type
        """
        sql_version = sql_version or self._get_new_version()

        if "auto" == sql_version:
            detected_version_on_system = self._detect_version_if_auto()
            if detected_version_on_system != "+":
                if detected_version_on_system == "":
                    print("Unknown SQL VERSION", file=sys.stderr)
                    sys.exit(1)
                else:
                    sql_version = detected_version_on_system
        return sql_version

    def get_repo_name(self, sql_version):
        return "cl-%s-common.repo" % self.REPO_NAMES.get(sql_version, None)

    def _load_new_packages(self, beta, sql_version=None, folder="new"):
        """
        detect and download packages for new installation
        """
        print(bcolors.info("Started downloading packages for the new installation"))
        # based on sql_version get packages names list and repo name
        packages, requires = [], []
        module = None
        arch = ".x86_64" if os.uname()[-1] == "x86_64" else ""
        sql_version = self._get_result_mysql_version(sql_version)

        if "auto" == sql_version:
            repo = "mysql-common.repo"
            if self.cl_version == 7:
                packages = ["mariadb", "mariadb-server", "mariadb-devel", "mariadb-libs", "mariadb-bench"]
            else:
                packages = ["mysql", "mysql-server", "mysql-libs", "mysql-devel"]
                if self.cl_version < 8:
                    packages.append("mysql-bench")

            module = self.MODULE_STREAMS.get(sql_version, None)
            cl8_module_enable(module)
            # download and install only need arch packages
            packages = ["%s%s" % (x, arch) for x in packages]
            for line in exec_command("yum info %s" % packages[0]):
                if line.startswith("Version"):
                    sql_version = "%s%s" % (
                        packages[0].split('.')[0], "".join(line.split(":")[1].split(".")[:2]).strip())

        else:
            repo = self.get_repo_name(sql_version)
            module = self.MODULE_STREAMS.get(sql_version, None)

            if sql_version.startswith("mysql"):
                packages = ["cl-MySQL-meta", "cl-MySQL-meta-client",
                            "cl-MySQL-meta-devel"]
                requires = list(packages)

                # if sql_version in ["mysql56", "mysql57"]:
                # packages.append("libaio%s" % arch)

            elif sql_version.startswith("mariadb"):
                packages = ["cl-MariaDB-meta", "cl-MariaDB-meta-client",
                            "cl-MariaDB-meta-devel"]
                requires = packages[:3]
            elif sql_version.startswith("percona"):
                packages = ["cl-Percona-meta", "cl-Percona-meta-client",
                            "cl-Percona-meta-devel"]
                requires = packages[:3]
            else:
                print("Unknown SQL VERSION", file=sys.stderr)
                sys.exit(1)

        if sql_version == "mysql51":
            packages += ["mysqlclient18", "mysqlclient15"]
        elif sql_version.startswith('mysql'):
            # Install mysqlclient18 for all versions of mysql, even for 5.5 and 5.6
            # because from now it contains also libmariadb support needed
            # for some packages like net-snmp-agent-libs on CL8
            if self.cl_version < 9:
                packages += ["mysqlclient16", "mysqlclient15", "mysqlclient18"]
            else:
                packages += ['mysqlclient18']

            if sql_version in ["mysql57", "mysql80"]:
                packages += ["numactl-devel%s" % arch, "numactl%s" % arch]
        elif sql_version.startswith("mariadb"):
            if self.cl_version < 9:
                packages += ["mysqlclient16", "mysqlclient15"]
            else:
                packages += ['mysqlclient18']
            if sql_version in ["mariadb55", "mariadb100", "mariadb101"]:
                packages += ["mysqlclient18-compat"]
            # Reasons for installation of mysqlclient18-compat:
            # it provides libmysqlclient.so.18 which is needed for postfix on CL7 (old versions of cl-MariaDB claim that they provide libmysqlclient.so.18 but doesn't actually install it)
            # it provides libmariadb.so.3() and libmariadb.so.3(libmysqlclient_18) for net-snmp-agent-libs on CL8
            #
            # Details about libmariadb:
            # Old versions of MariaDB up to 10.1 do not contain libmariadb at all.
            # So we need to install mysqlclient18-compat for mariadb55, mariadb100 and mariadb101.
            # cl-MariaDB-102-libs and cl-MariaDB-103-libs pkgs always contained libmariadb.so.3
            # but started to declare that they provide libmariadb.so.3() only from 10.2.44-3 and 10.3.39-3

            # Case from CLOS-2115: the customer with cl-MariaDB105 wants to install soci-mysql pkg
            # which requires libmysqlclient.so.21. So we need to install mysqlclient21 with every clMariaDB
            # for every CL version supported by mysqlclient21 package, that is on CL7+
            if self.cl_version >= 7:
                packages += ['mysqlclient21']
        elif sql_version.startswith("percona"):
            packages += ["mysqlclient18", "mysqlclient16", "mysqlclient15"]

        packages.append("libaio%s" % arch)

        repo_url = "http://repo.cloudlinux.com/other/cl%s/mysqlmeta/%s" % (
            self.cl_version, repo)

        try:
            content = urllib.request.urlopen(repo_url).read()
        except Exception as e:
            print("Can`t download repo file(%s): %s" % (repo_url, e), file=sys.stderr)
            #sys.exit(1)
            # Use default cl-mysql repo with mysqlclient only
            # expecting that meta packages are already available somewhere in predefined repos
            if os.path.exists("/etc/yum.repos.d/cl-mysql.repo"):
                shutil.copy2("/etc/yum.repos.d/cl-mysql.repo",
                             "/etc/yum.repos.d/cl-mysql.repo.bak")
            shutil.copy2("/usr/share/lve/dbgovernor/cl-mysql.repo.default",
                         "/etc/yum.repos.d/cl-mysql.repo")
            default_cl_mysql_repo = True
        else:
            if os.path.exists("/etc/yum.repos.d/cl-mysql.repo"):
                shutil.copy2("/etc/yum.repos.d/cl-mysql.repo",
                             "/etc/yum.repos.d/cl-mysql.repo.bak")
            write_file("/etc/yum.repos.d/cl-mysql.repo", content.decode())
            default_cl_mysql_repo = False

        # update repositories
        exec_command_out("yum clean all")
        cl8_module_enable(module)

        # Add requires to packages list
        if default_cl_mysql_repo:
            # We did not find specific repo for meta pkgs and installed default one instead,
            # So let's try to find meta pkgs in any repo
            for name in requires:
                packages += exec_command("repoquery --requires %s --quiet" % name)
        else:
            # We found specific repo for meta pkgs and installed it,
            # So let's try to find meta pkgs in it only
            for name in requires:
                req_packages = exec_command(f"repoquery --repoid cl-mysql-meta --requires {name} --quiet")
                if len(req_packages):
                    packages += req_packages
                else:
                    # in case of repos are already created but still empty - fallback to any repo case
                    packages += exec_command(f"repoquery --requires {name} --quiet")

        if not download_packages(packages, folder, beta):
            self.ALL_PACKAGES_NEW_NOT_DOWNLOADED = True

        return packages

    def get_mysql_user(self):
        """
        Retrieve MySQL user name and password and save it into self attributes
        """

    @staticmethod
    def _check_mysql_version():
        """
        Retrieve MySQL version from mysql --version command
        :return: dictionary with version of form {
                short: two numbers of version (e.g. 5.5)
                extended: all numbers of version (e.g. 5.5.52)
                mysql_type: type flag (mysql or mariadb)
                full: mysql_type + short version (e.g. mariadb55)
            }
        """
        try:
            version_string = exec_command('mysql --version', silent=True)
            version_info = re.findall(r'(?<=Distrib\s)[^,]+', version_string[0])
            if not version_info:
                # for mysql 8.0
                version_info = re.findall(r'(?<=Ver\s)\S+', version_string[0])
                parts = version_info[0].split(' ')
            else:
                parts = version_info[0].split('-')
            version = {
                'short': '.'.join(parts[0].split('.')[:-1]),
                'extended': parts[0],
                'mysql_type': parts[1].lower() if len(parts) > 1 else 'mysql'
            }
            version.update({'full': '{m_type}{m_version}'.format(m_type=version['mysql_type'],
                                                                 m_version=version['short'].replace('.', ''))})
        except Exception:
            return {}
        return version

    def check_need_for_mysql_upgrade(self):
        """
        Basic check for upgrading MySQL tables
        The True condition for mysql_upgrade is
            if mysql type has changed (mysql/mariadb)
            if version has changed
        :return: should upgrade or not (True or False)
        """
        current_version = self._check_mysql_version()
        if not self.prev_version or not current_version:
            print('Problem with version retrieving')
            return False
        return current_version['mysql_type'] != self.prev_version['mysql_type'] or current_version['short'] != self.prev_version['short']

    def run_mysql_upgrade(self):
        """
        Run mysql_upgrade and mysql_fix_privilege_tables scripts if it is needed
        """
        print('Check for the need of mysql_upgrade...')
        if self.check_need_for_mysql_upgrade():
            print('Tables should be upgraded!')
            if self.MYSQLPASSWORD:
                cmd_upgrade = "/usr/bin/mysql_upgrade --user='{user}' --password='{passwd}'".format(user=self.MYSQLUSER, passwd=self.MYSQLPASSWORD)
                cmd_fix = "/usr/bin/mysql_fix_privilege_tables --user='{user}' --password='{passwd}'".format(user=self.MYSQLUSER, passwd=self.MYSQLPASSWORD)
            else:
                cmd_upgrade = '/usr/bin/mysql_upgrade'
                cmd_fix = '/usr/bin/mysql_fix_privilege_tables'
            exec_command_out(cmd_upgrade)
            if os.path.exists('/usr/bin/mysql_fix_privilege_tables'):
                exec_command_out(cmd_fix)
        else:
            print('No need for upgrading tables')

    def _before_install_new_packages(self):
        """
        Specific actions before install new packages
        """

    def _after_install_new_packages(self):
        """
        Specific actions after install new packages
        """
        # patch governor config if needed
        self._set_mysql_access()
        # run mysql_upgrade if needed
        self.run_mysql_upgrade()
        print("The installation of MySQL for db_governor completed")

    def _after_install_rollback(self):
        """
        Panel-specified actions trigger for rollback bad installation
        """

    def _before_remove_old_packages(self):
        """
        Specific actions before removing old packages
        """
        # stop mysql server
        self._mysqlservice("stop")

    def _after_remove_old_packages(self):
        """
        Specific actions after removing old packages
        """

    def _before_delete(self):
        """
        Specific actions before delete
        """

    def _delete(self, installed_packages):
        """
        Remove installed packages and install new
        """
        print(bcolors.info("Removing mysql for db_governor start"))

        # download standard packages
        self._load_new_packages(False, "auto")

        # if not os.path.exists("/etc/my.cnf.bkp"):
        # shutil.copy2("/etc/my.cnf", "/etc/my.cnf.govprev")
        self.my_cnf_manager('backup')  # why without if exists?

        self._mysqlservice("stop")

        # remove governor package
        if IS_UBUNTU:
            exec_command_out('apt remove governor-mysql -y')
        else:
            exec_command_out("rpm -e governor-mysql")

        # delete installed packages
        remove_packages(installed_packages)

        # install auto packages
        if IS_UBUNTU:
            install_deb_packages('new')
        else:
            install_packages("new", False)
        self.cl8_save_current()

        print(bcolors.ok("Removing mysql for db_governor completed"))

    def _after_delete(self):
        """
        Specific actions after delete
        """
        force_update_cagefs()

    def _before_install(self):
        """
        Specific actions before governor installation
        """

    def _after_install(self):
        """
        Specific actions after governor installation
        """
        #force_update_cagefs()

    def _set_mysql_access(self):
        """
        Set mysql admin login and password and save it to governor config
        """
        self.get_mysql_user()
        if self.MYSQLUSER and self.MYSQLPASSWORD:
            print("Patch governor configuration file")
            check_file("/etc/container/mysql-governor.xml")
            patch_governor_config(self.MYSQLUSER, self.MYSQLPASSWORD)

            if is_package_installed('governor-mysql'):
                service("restart", "db_governor")
                print("DB-Governor restarted...")

    @staticmethod
    def _kill_mysql():
        """
        Kill mysqld processes.
        """
        if check_mysqld_is_alive():
            print("Stop hunging MySQL")
            exec_command_out("/usr/bin/killall -SIGTERM mysqld_safe")
            print("Waiting for mysqld_safe stop")
            time.sleep(10)
            exec_command_out("/usr/bin/killall -SIGTERM mysqld")
            print("Waiting for mysqld stop")
            time.sleep(10)

    def _enable_mariadb(self):
        """
        Enable mariaDB services
        """
        if self.cl_version >= 7:
            exec_command_out("systemctl enable mariadb.service")
            # MariaDB service file declare mysql and mysqld as its aliases
            # So they will be created as symlinks by "enable mariadb.service" command
            # Then those two commands lead to error like this:
            # Failed to execute operation: Too many levels of symbolic links
            #exec_command_out("systemctl enable mysql.service")
            #exec_command_out("systemctl enable mysqld.service")

    def _enable_mysql(self):
        """
        Enable MySQL services
        """
        if self.cl_version >= 7:
            exec_command_out("systemctl enable mysql.service")
            exec_command_out("systemctl enable mysqld.service")

    def _enable_percona(self):
        """
        Enable Percona service
        """
        if self.cl_version >= 7:
            exec_command_out("systemctl enable mysql.service")

    def _check_leave_pid(self):
        """
        Remove upgrade marker for mysql
        """
        print("Check for mysql pids and upgrade marker")
        if os.path.exists(f"{self.my_cnf_datadir}/RPM_UPGRADE_MARKER"):
            shutil.move(f"{self.my_cnf_datadir}/RPM_UPGRADE_MARKER",
                        f"{self.my_cnf_datadir}/RPM_UPGRADE_MARKER.old")

    @staticmethod
    def _ld_fix():
        """
        Fix shared library problems
        """
        if os.path.exists("/usr/lib64/mysql/libmygcc.a"):
            os.rename("/usr/lib64/mysql/libmygcc.a",
                      "/usr/lib64/mysql/libmygcc.a.bak")

        if os.path.exists("/usr/lib/mysql/libmygcc.a"):
            os.rename("/usr/lib/mysql/libmygcc.a",
                      "/usr/lib/mysql/libmygcc.a.bak")

        if os.path.exists("/sbin/ldconfig"):
            exec_command_out("/sbin/ldconfig")

    def _get_new_version(self):
        """
        Get new sql version for install
        """
        if os.path.exists(self.NEW_VERSION_FILE):
            return read_file(self.NEW_VERSION_FILE)
        return "auto"

    def _save_previous_version(self):
        """
        Save current installed mysql version to cache file.
        It will be previous version after new installation.
        """
        version = mysql_version()
        if version:
            write_file(self.CACHE_VERSION_FILE, version)

    def _get_previous_version(self):
        """
        Get current installed mysql version from cache file
        """
        if os.path.exists(self.CACHE_VERSION_FILE):
            return read_file(self.CACHE_VERSION_FILE)
        return 'auto'

    def _mysqlservice(self, action):
        """
        Stop mysql service
        """
        version = mysql_version()
        name = "mysql" if version in ["percona56", "mariadb55", "mariadb100"] \
            else "mysqld"
        if 6 == self.cl_version:
            if version in ["mysql51", "mysql55", "mysql56",
                           "mysql57", "mysql80", "mariadb101", "mariadb102",
                           "mariadb103", "mariadb104", "mariadb105", "mariadb106"]:
                name = "mysql"

        try:
            # service util now uses timeout
            service(action, name)
        except RuntimeError as e:
            print("Failed to {act} mysql service: {exc}. Please, check mysql service status and logs.".format(
                act=action, exc=e))
            sys.exit(3)

    def _rel(self, path):
        """
        Get absolute path based on installed directory
        """
        return os.path.join(self.SOURCE, path)

    def rel(self, path):
        """
        Public wrapper for _rel
        """
        return self._rel(path)

    def _script(self, path, args=None):
        """
        Execute package script which locate in SOURCE directory
        """
        exec_command_out("%s %s" % (self._rel("scripts/%s" % path), args or ""))

    def _script_subprocess(self, path, args=None):
        """
        Execute package script which locate in SOURCE directory
        """
        exec_command("%s %s" % (self._rel("scripts/%s" % path), args or ""))

    def _detect_version_if_auto(self):
        """
        What should we do if we get auto param in mysql.type
        """
        return "+"

    def _custom_download_of_rpm(self, package_name):
        """
        How we should to download installed MySQL package
        """
        return "no"

    def _custom_rpm_installer(self, package_name, indicator=False):
        """
        Specific rpm installer to use for given package name
        :param package_name: package to install
        :param indicator:
        :return:
        """
        return "no"

    def make_additional_panel_related_check(self):
        """
        Specific cPanel
        :return:
        """
        return

    def unsupported_db_version(self, force=False):
        """
        Skip an installation if not supported db version has been set:
        Update fom mysql80 to MariaDB 10.x version is not supported
        """
        current_version = self._check_mysql_version()
        if current_version.get('full') == 'mysql80':
            version = InstallManager._get_result_mysql_version(self)
            if version.startswith('mariadb10'):
                print(bcolors.fail(
                    "!!! WARNING !!!\n"
                    "Upgrade from MySQL 8 to MariaDB 10.x isn't supported due to compatibility\n"
                    "issues and will likely lead to a disaster / break your database server completely.\n"
                    "In order to save you, we've disabled this upgrade in DB Governor.\n"
                    "!!! WARNING !!!\n\n"
                    "A detailed explanation and workaround for CloudLinux can be found in these articles:\n"
                    "MariaDB info about the compatibility issue: "
                    "https://mariadb.com/kb/en/upgrading-from-mysql-to-mariadb/\n"
                    "Workaround for CloudLinux: "
                    "https://cloudlinux.zendesk.com/hc/en-us/articles/360020599839"
                ))
                if not force:
                    sys.exit(1)

    def prepare_statement_for_ubuntu(self):
        """Specific actions before governor installation"""

Zerion Mini Shell 1.0