Sindbad~EG File Manager
| Current Path : /opt/dedrads/ |
|
|
| Current File : //opt/dedrads/mysql_selector.py |
#!/usr/bin/env python3
import sys, os, re, json, shlex, subprocess, datetime, time, socket, glob, shutil
from shutil import which
TARGETS = {
"mariadb1011": ("mariadb", "10.11"),
"mariadb106": ("mariadb", "10.6"),
"mariadb114": ("mariadb", "11.4"),
"mysql80": ("mysql", "8.0"),
"mysql84": ("mysql", "8.4"),
}
CPANEL_CONFIG = "/var/cpanel/cpanel.config"
DATADIR = "/var/lib/mysql"
BACKUP_ROOT = "/root"
ROOT_MYCNF = "/root/.my.cnf"
LOG_FILE_DEFAULT = None # initialized after ts() is defined
LOG_FH = None
DRY_RUN = False
FORCE_DELETE = False
MAX_ALLOWED_PACKET = "512M"
ORIGINAL_MAX_ALLOWED_PACKET = None
_RPM_CACHE = None
def print_help(prog):
"""Display detailed usage guidance."""
help_text = f"""mysql_selector.py - Install or switch MySQL/MariaDB on cPanel (AlmaLinux 8+)
Usage:
{prog} [--dry-run] [--force] [--log-file=/path/to/log] <target>
Targets (key -> engine/version):
mariadb1011 -> MariaDB 10.11 (LTS)
mariadb106 -> MariaDB 10.6
mariadb114 -> MariaDB 11.4
mysql80 -> MySQL 8.0
mysql84 -> MySQL 8.4
What this script does:
- Verifies the host is AlmaLinux 8 or newer.
- Backs up all non-system databases to {BACKUP_ROOT}/mysql-backup-<timestamp>.
- Stops existing MySQL/MariaDB services and removes conflicting packages.
- Sets the requested version in cPanel config, then triggers WHM's installer.
- Falls back to manual installs for supported versions if WHM install fails.
- Ensures /root/.my.cnf credentials work and restores databases + grants.
- Writes a log to {LOG_FILE_DEFAULT or "<default>"} (or a custom --log-file).
Expectations and prerequisites:
- Run as root on a cPanel server (whmapi1 required for the primary path).
- /root/.my.cnf must contain working root credentials; backups are not deleted.
- Sufficient free space (>5%% headroom) is required on {BACKUP_ROOT} for backups.
Options:
--dry-run Skip destructive commands (package removals, globs) and prompt before deletion.
--force Proceed with deletions/removals without prompting (overrides prompts).
--log-file=PATH Write log output to PATH instead of the default in /root.
-h, --help Show this help.
Exit codes:
0 on success, non-zero on error.
Examples:
{prog} mariadb114
{prog} mysql84
"""
print(help_text)
def ts():
return datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
LOG_FILE_DEFAULT = f"/root/mysql_selector-{ts()}.log"
_REAL_PRINT = print
def init_logging(log_file=None):
"""Initialize logging to both stdout and a file."""
global LOG_FH, LOG_FILE_DEFAULT, _REAL_PRINT
target = log_file or LOG_FILE_DEFAULT
try:
LOG_FH = open(target, "a")
LOG_FILE_DEFAULT = target
except Exception as e:
_REAL_PRINT(f"[!] Unable to open log file {target}: {e}")
LOG_FH = None
return
def log_print(*args, **kwargs):
_REAL_PRINT(*args, **kwargs)
if LOG_FH:
text = " ".join(str(a) for a in args)
end = kwargs.get("end", "\n")
try:
LOG_FH.write(text + end)
LOG_FH.flush()
except Exception:
pass
globals()["print"] = log_print
def check_os_support():
"""Ensure we are running on AlmaLinux 8 or newer."""
try:
info = {}
with open("/etc/os-release") as f:
for line in f:
if "=" in line:
k, v = line.strip().split("=", 1)
info[k] = v.strip('"')
distro = info.get("NAME", "")
version = info.get("VERSION_ID", "")
except Exception:
distro = ""
version = ""
if "almalinux" not in distro.lower():
print("[!] This script supports only AlmaLinux systems.")
sys.exit(1)
try:
major = int(version.split(".")[0])
except (ValueError, IndexError):
major = 0
if major < 8:
print(
"[!] AlmaLinux version {} detected. Version 8 or newer required.".format(
version or "<unknown>"
)
)
sys.exit(1)
def run(cmd, check=True, destructive=False):
if DRY_RUN and destructive:
print(f"[dry-run] Skipping command: {cmd}")
return 0
if isinstance(cmd, str):
cmd = shlex.split(cmd)
print("+ " + shlex.join(cmd))
p = subprocess.run(cmd)
if check and p.returncode != 0:
sys.exit(p.returncode)
return p.returncode
def out(cmd):
try:
if isinstance(cmd, str):
cmd = shlex.split(cmd)
return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode(
"utf-8", "replace"
)
except subprocess.CalledProcessError as e:
return (e.output or b"").decode("utf-8", "replace")
except FileNotFoundError:
return ""
def run_background(cmd):
"""Run a command in the background, suppressing stdout/stderr, and return the Popen object."""
if isinstance(cmd, str):
cmd = shlex.split(cmd)
if DRY_RUN:
print(f"[dry-run] Skipping background command: {cmd}")
return None
print("+ " + shlex.join(cmd) + " (background)")
try:
return subprocess.Popen(
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
except Exception as e:
print(f"[!] Failed to start background command {cmd}: {e}")
return None
def installed_family():
pkgs = out(["rpm", "-qa"])
if re.search(r"\bmysql-community-server\b", pkgs, re.I) or re.search(
r"\bmysql-server\b", pkgs, re.I
):
return "mysql"
if re.search(r"\bMariaDB-server\b", pkgs, re.I):
return "mariadb"
return "none"
def ping_mysql():
mysqladmin = which("mysqladmin")
if not mysqladmin:
return False
rc = run([mysqladmin, "ping"], check=False)
return rc == 0
def ensure_dir(d):
if not os.path.isdir(d):
os.makedirs(d, exist_ok=True)
def remove_matches(patterns, force=None):
"""Remove files/dirs matching glob patterns without invoking a shell."""
force = FORCE_DELETE if force is None else force
matches = []
for pat in patterns:
matches.extend(glob.glob(pat))
matches = sorted(set(matches))
if not matches:
return
print("[*] Paths matched for removal:")
for path in matches:
print(f" {path}")
if DRY_RUN:
print("[dry-run] Skipping deletion of matched paths.")
return
if not force:
ans = input("[?] Remove these paths? [y/N]: ").strip().lower()
if ans not in ("y", "yes"):
print("[!] Skipping removal by user request.")
return
for path in matches:
try:
if os.path.isdir(path) and not os.path.islink(path):
shutil.rmtree(path, ignore_errors=True)
else:
os.remove(path)
except FileNotFoundError:
continue
except Exception as e:
print(f"[!] Unable to remove {path}: {e}")
def rpm_packages_matching(regex, refresh=False):
"""Return rpm -qa entries matching regex (case-insensitive)."""
global _RPM_CACHE
if refresh or _RPM_CACHE is None:
_RPM_CACHE = out(["rpm", "-qa"]).splitlines()
pattern = re.compile(regex, re.I)
return [p for p in _RPM_CACHE if pattern.search(p)]
def invalidate_rpm_cache():
"""Clear cached rpm -qa results after package changes."""
global _RPM_CACHE
_RPM_CACHE = None
def estimate_datadir_size():
"""Return total size of DATADIR in bytes using du -sb."""
try:
output = subprocess.check_output(
["du", "-sb", DATADIR], stderr=subprocess.STDOUT
).decode("utf-8", "replace")
size_str = output.strip().split()[0]
return int(size_str)
except subprocess.CalledProcessError as e:
print(
f"[!] Unable to measure {DATADIR} size: {e.output.decode('utf-8', 'replace')}"
)
except Exception as e:
print(f"[!] Unable to measure {DATADIR} size: {e}")
return None
def has_sufficient_space_for_backup():
"""Ensure creating a backup won't exceed 95% capacity on the backup filesystem."""
try:
stat = os.statvfs(BACKUP_ROOT)
except Exception as e:
print(
f"[!] Could not determine filesystem stats for {BACKUP_ROOT}: {e}"
)
return False
total = stat.f_blocks * stat.f_frsize
avail = stat.f_bavail * stat.f_frsize
datadir_size = estimate_datadir_size()
if datadir_size is None:
print("[!] Unable to determine MySQL data size; refusing to continue.")
return False
min_free = total * 0.05 # require 5% free space remaining
projected_free = avail - datadir_size
print(
f"[i] Backup filesystem total: {total / (1024**3):.2f} GB, free: {avail / (1024**3):.2f} GB, datadir size: {datadir_size / (1024**3):.2f} GB"
)
if projected_free < min_free:
print(
f"[!] Not enough free space: projected free {projected_free / (1024**3):.2f} GB would drop below 5% of capacity."
)
return False
return True
def backup_all(rootdir):
"""Dump all non-system databases and copy cPanel metadata if present.
Returns (backup_directory, status) where status in {"ok","empty","error"}.
"""
ensure_dir(rootdir)
bdir = os.path.join(rootdir, "mysql-backup-" + ts())
os.makedirs(bdir, exist_ok=True)
print(f"[*] Backing up to: {bdir}")
status = "ok"
if not ping_mysql():
print("[!] MySQL server is not running, cannot perform backup!")
return bdir, "error"
if not which("mysql") or not which("mysqldump"):
print(
"[!] mysql or mysqldump command not found, skipping database backup"
)
return bdir, "error"
user_dbs = []
try:
# Get list of all databases
print("[*] Getting list of databases...")
dbs_cmd = [which("mysql"), "-NBe", "SHOW DATABASES"]
dbs = out(dbs_cmd).splitlines()
if not dbs:
print("[*] No databases found to back up")
else:
# Filter out system databases
system_dbs = {
'mysql',
'information_schema',
'performance_schema',
'sys',
}
user_dbs = [
db.strip()
for db in dbs
if db.strip() and db.strip() not in system_dbs
]
if not user_dbs:
print("[*] No user databases found to back up")
return bdir, "empty"
else:
print(
"[*] Backing up user databases: {}".format(", ".join(user_dbs))
)
# Backup each database separately for better reliability
for db in user_dbs:
try:
backup_file = os.path.join(bdir, f"{db}.sql")
print(f"[*] Backing up database: {db}")
# Build the mysqldump command
dump_cmd = [
which("mysqldump"),
"--defaults-file=/root/.my.cnf",
"--single-transaction",
"--quick",
"--routines",
"--events",
"--triggers",
"--hex-blob",
db,
]
# Execute the command and write output to file
with open(backup_file, 'w') as f:
subprocess.run(dump_cmd, stdout=f, check=True)
print(f"[+] Successfully backed up {db} to {backup_file}")
except subprocess.CalledProcessError as e:
print(f"[!] Error backing up database {db}: {e}")
status = "error"
if os.path.exists(backup_file):
os.remove(backup_file)
except Exception as e:
print(
f"[!] Unexpected error backing up database {db}: {str(e)}"
)
status = "error"
if os.path.exists(backup_file):
os.remove(backup_file)
except Exception as e:
print(f"[!] Error during backup process: {str(e)}")
status = "error"
# Backup cPanel databases configuration
if os.path.isdir("/var/cpanel/databases"):
run(
[
"/usr/bin/rsync",
"-aH",
"/var/cpanel/databases/",
os.path.join(bdir, "cpanel_databases"),
],
check=False,
)
return bdir, status
def restore_all_databases(backup_dir):
"""Restore all SQL dumps found in backup_dir (user DBs only). Returns True on success."""
if not backup_dir or not os.path.isdir(backup_dir):
print("[!] Backup directory missing or invalid, skipping restore")
return False
set_server_max_allowed_packet(MAX_ALLOWED_PACKET)
if not wait_for_mysql_ready(timeout=600, interval=5, require_auth=True):
print(
"[!] MySQL/MariaDB service is not ready; cannot restore databases"
)
return False
mysql_bin = which("mysql")
if not mysql_bin:
print("[!] mysql client not found, skipping restore")
return False
sql_files = sorted(
[f for f in os.listdir(backup_dir) if f.endswith(".sql")]
)
if not sql_files:
print(f"[*] No SQL dumps found to restore in {backup_dir}")
return True
restore_ok = True
for fname in sql_files:
db = os.path.splitext(fname)[0]
if db in ("mysql", "sys"):
continue
fpath = os.path.join(backup_dir, fname)
print(f"[*] Restoring database: {db} from {fpath}")
try:
# Ensure the database exists before import
create_stmt = f"CREATE DATABASE IF NOT EXISTS `{db}` DEFAULT CHARACTER SET utf8mb4"
run(
[
mysql_bin,
"--defaults-file=/root/.my.cnf",
f"--max_allowed_packet={MAX_ALLOWED_PACKET}",
"-e",
create_stmt,
],
check=False,
)
with open(fpath) as f:
subprocess.run(
[
mysql_bin,
"--defaults-file=/root/.my.cnf",
f"--max_allowed_packet={MAX_ALLOWED_PACKET}",
db,
],
stdin=f,
check=True,
)
print(f"[+] Restored {db}")
except subprocess.CalledProcessError as e:
print(f"[!] Failed to restore {db}: {e}")
restore_ok = False
# Restore cPanel database metadata if present
cp_src = os.path.join(backup_dir, "cpanel_databases")
if os.path.isdir(cp_src):
run(
["/usr/bin/rsync", "-aH", cp_src + "/", "/var/cpanel/databases/"],
check=False,
)
print("[*] Restored /var/cpanel/databases metadata")
return restore_ok
def stop_db():
run(["systemctl", "stop", "mysqld"], check=False)
run(["systemctl", "stop", "mysql"], check=False)
run(["systemctl", "stop", "mariadb"], check=False)
def park_datadir():
if os.path.isdir(DATADIR) and not os.path.islink(DATADIR):
parked = DATADIR + ".off-" + ts()
print(f"[*] Parking datadir: {DATADIR} -> {parked}")
run(["mv", DATADIR, parked], check=True, destructive=True)
def get_current_mysql_version():
try:
output = out(["mysql", "--version"])
if not output:
return None
# Extract version number (e.g., 'mysql Ver 8.0.33 for Linux on x86_64' -> '8.0')
match = re.search(r'(?:Ver )?(\d+\.\d+)', output)
if match:
return match.group(1)
except Exception as e:
print(f"[!] Could not determine current MySQL version: {e}")
return None
def remove_mysql_family():
print("[*] Stopping MySQL service and parking datadir...")
stop_db()
park_datadir()
print("[*] Disabling MySQL module and removing repositories...")
run(["dnf", "-y", "module", "reset", "mysql"], check=False)
run(["dnf", "-y", "module", "disable", "mysql"], check=False)
remove_matches(
[
"/etc/yum.repos.d/mysql*.repo",
"/etc/yum.repos.d/mysql*-community*.repo",
"/etc/yum/repos.d/mysql*.repo",
]
)
print("[*] Removing MySQL packages...")
# Find all MySQL packages
mysql_pkgs = rpm_packages_matching(r"^(mysql|mysql-community|mysql[0-9])")
if mysql_pkgs:
print(f"[*] Found MySQL packages to remove: {' '.join(mysql_pkgs)}")
# First try dnf remove
run(["dnf", "-y", "remove"] + mysql_pkgs, check=False, destructive=True)
invalidate_rpm_cache()
# Then force remove any remaining packages
remaining = rpm_packages_matching(
r"^(mysql|mysql-community|mysql[0-9])", refresh=True
)
if remaining:
print(
f"[*] Force removing remaining MySQL packages: {' '.join(remaining)}"
)
run(
["rpm", "-e", "--nodeps"] + remaining,
check=False,
destructive=True,
)
invalidate_rpm_cache()
# Additional cleanup for common MySQL-related packages
extra_pkgs = rpm_packages_matching(
r"^(mysql-community|mysql80-community|mysql84-community|numactl-libs)",
refresh=True,
)
if extra_pkgs:
run(["dnf", "-y", "remove"] + extra_pkgs, check=False, destructive=True)
invalidate_rpm_cache()
# Clean up any remaining MySQL files and directories
print("[*] Cleaning up MySQL files and directories...")
remove_matches(
[
"/var/lib/mysql*",
"/var/run/mysql*",
"/etc/my.cnf*",
"/etc/mysql*",
"/usr/lib64/mysql*",
"/usr/share/mysql*",
]
)
# Clean up any remaining dependencies
run(["dnf", "-y", "autoremove"], check=False, destructive=True)
invalidate_rpm_cache()
invalidate_rpm_cache()
invalidate_rpm_cache()
run(["dnf", "clean", "all"], check=False)
print("[*] MySQL removal completed")
def remove_mariadb_family():
print("[*] Stopping MariaDB service and parking datadir...")
stop_db()
park_datadir()
print("[*] Disabling MariaDB module and removing repositories...")
# Remove any MariaDB modules
run(["dnf", "-y", "module", "reset", "mariadb"], check=False)
run(["dnf", "-y", "module", "disable", "mariadb"], check=False)
# Remove all MariaDB repositories
remove_matches(
[
"/etc/yum.repos.d/MariaDB*.repo",
"/etc/yum/repos.d/mariadb*.repo",
"/etc/yum.repos.d/mysql*.repo",
]
)
print("[*] Removing all MySQL and MariaDB packages...")
# First, try to remove everything using dnf
pkg_list = rpm_packages_matching(
r"(mariadb|mysql|galera|socat|boost-.*mariadb)", refresh=True
)
if pkg_list:
run(["dnf", "-y", "remove"] + pkg_list, check=False, destructive=True)
invalidate_rpm_cache()
# Force remove any remaining packages
remaining_pkgs = rpm_packages_matching(
r"(mariadb|mysql|galera)", refresh=True
)
if remaining_pkgs:
print(
f"[*] Found remaining packages to remove: {' '.join(remaining_pkgs)}"
)
run(
["rpm", "-e", "--nodeps"] + remaining_pkgs,
check=False,
destructive=True,
)
invalidate_rpm_cache()
print("[*] Cleaning up configuration files and directories...")
# Clean up all MySQL/MariaDB related files and directories
remove_matches(
[
"/var/lib/mysql*",
"/var/run/mariadb*",
"/var/run/mysqld*",
"/etc/my.cnf*",
"/etc/mysql*",
"/etc/mariadb*",
"/usr/lib64/mysql*",
"/usr/share/mysql*",
"/usr/bin/mysql*",
"/usr/sbin/mysql*",
]
)
# Clean up any remaining dependencies and clear cache
run(["dnf", "-y", "autoremove"], check=False, destructive=True)
run(["dnf", "clean", "all"], check=False)
print("[*] MariaDB/MySQL removal completed")
def ensure_perl_mysql_modules():
"""Install required Perl modules used by cPanel DB tooling."""
# Use cPanel tooling to satisfy bundled Perl modules.
check_pkgs = "/usr/local/cpanel/scripts/check_cpanel_pkgs"
if os.path.exists(check_pkgs):
print("[*] Ensuring cPanel Perl modules via check_cpanel_pkgs --fix")
run([check_pkgs, "--fix"], check=False)
else:
print(
"[!] /usr/local/cpanel/scripts/check_cpanel_pkgs not found; skipping Perl module fix"
)
def set_cpanel_mysql_version(ver):
print(
"[*] Setting mysql-version={} in {} (Python write)".format(
ver, CPANEL_CONFIG
)
)
try:
run(
["/bin/cp", "-a", CPANEL_CONFIG, CPANEL_CONFIG + ".bak-" + ts()],
check=False,
)
except Exception:
pass
try:
with open(CPANEL_CONFIG) as f:
lines = f.read().splitlines(True)
except Exception:
lines = []
found = False
new_lines = []
for line in lines:
if line.strip().startswith("mysql-version="):
new_lines.append(f"mysql-version={ver}\n")
found = True
else:
new_lines.append(line)
if not found:
if new_lines and not new_lines[-1].endswith("\n"):
new_lines[-1] = new_lines[-1] + "\n"
new_lines.append(f"mysql-version={ver}\n")
tmp = CPANEL_CONFIG + ".tmp"
with open(tmp, "w") as f:
f.writelines(new_lines)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, CPANEL_CONFIG)
print(f"[+] mysql-version set to {ver}")
# verify
cur = ""
try:
with open(CPANEL_CONFIG) as f:
for l in f:
if l.strip().startswith("mysql-version="):
cur = l.strip()
break
except Exception:
cur = "<unreadable>"
print("[=] [{}] -> {}".format(CPANEL_CONFIG, cur or "<missing>"))
def whm_allowed():
raw = out(
[
"/usr/sbin/whmapi1",
"start_background_mysql_upgrade",
"version=__probe__",
"--output=json",
]
)
allowed = []
try:
j = json.loads(raw)
reason = (j.get("metadata") or {}).get("reason", "")
except Exception:
reason = raw
vers = re.findall(r"[“\"]([0-9]+\.[0-9]+)[”\"]", reason)
if vers:
seen = set()
allowed = [v for v in vers if not (v in seen or seen.add(v))]
return allowed
def whm_start_upgrade(ver):
print("[*] Triggering WHM MySQL/MariaDB install/upgrade …")
# Remove --allow-downgrade as it's not a valid option
rc = run(
[
"/usr/sbin/whmapi1",
"start_background_mysql_upgrade",
f"version={ver}",
],
check=False,
)
if rc != 0:
print("[!] WHM did not accept the request.")
return False
print("=== TRIGGERED ===")
print("Track with: whmapi1 get_mysql_upgrade --output=json|yaml")
print(
"If datadir was parked, restore **after** engine is up, then run: /usr/local/cpanel/bin/restoregrants --force"
)
return True
def wait_for_mysql_ready(timeout=1800, interval=10, require_auth=False):
"""Wait until MySQL responds to mysqladmin ping.
If require_auth=False, treat 'Access denied' as service-ready."""
start = time.time()
while time.time() - start < timeout:
mysqladmin = which("mysqladmin")
if not mysqladmin:
time.sleep(interval)
continue
proc = subprocess.run(
[mysqladmin, "ping"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if proc.returncode == 0:
return True
output = proc.stdout.decode("utf-8", "replace")
if not require_auth and "Access denied" in output:
print(
"[i] MySQL service responding but root authentication failed; continuing."
)
return True
time.sleep(interval)
return False
def install_with_cpanel_api(target_ver):
print(f"[*] Requesting MySQL/MariaDB {target_ver} via cPanel API...")
if not which("whmapi1"):
print("[!] whmapi1 not found; cannot use cPanel API installer")
return False
if not whm_start_upgrade(target_ver):
return False
print("[*] Waiting for database service to come online...")
if wait_for_mysql_ready():
print("[+] Database service is online after API request")
return True
print(
"[!] Database service did not come online in expected time; check whmapi1 get_mysql_upgrade"
)
return False
def set_server_max_allowed_packet(packet_size):
"""Raise server/client max_allowed_packet before imports."""
global ORIGINAL_MAX_ALLOWED_PACKET
def _current_packet():
mysql_bin_inner = which("mysql")
if not mysql_bin_inner or not os.path.exists(ROOT_MYCNF):
return None
try:
raw = out(
[
mysql_bin_inner,
"--defaults-file=" + ROOT_MYCNF,
"-NBe",
"SHOW VARIABLES LIKE 'max_allowed_packet'",
]
)
parts = raw.strip().split()
if parts:
return parts[-1]
except Exception:
return None
return None
mysql_bin = which("mysql")
if not mysql_bin or not os.path.exists(ROOT_MYCNF):
return
if ORIGINAL_MAX_ALLOWED_PACKET is None:
ORIGINAL_MAX_ALLOWED_PACKET = _current_packet()
try:
run(
[
mysql_bin,
"--defaults-file=" + ROOT_MYCNF,
"-e",
f"SET GLOBAL max_allowed_packet={packet_size};",
],
check=False,
)
except Exception as e:
print(f"[!] Unable to set max_allowed_packet to {packet_size}: {e}")
def restore_max_allowed_packet():
"""Revert max_allowed_packet to original value if it was changed."""
global ORIGINAL_MAX_ALLOWED_PACKET
if not ORIGINAL_MAX_ALLOWED_PACKET:
return
mysql_bin = which("mysql")
if not mysql_bin or not os.path.exists(ROOT_MYCNF):
return
try:
run(
[
mysql_bin,
"--defaults-file=" + ROOT_MYCNF,
"-e",
f"SET GLOBAL max_allowed_packet={ORIGINAL_MAX_ALLOWED_PACKET};",
],
check=False,
)
print(
f"[+] max_allowed_packet reverted to {ORIGINAL_MAX_ALLOWED_PACKET}"
)
except Exception as e:
print(
f"[!] Unable to restore max_allowed_packet to {ORIGINAL_MAX_ALLOWED_PACKET}: {e}"
)
def run_upcp_force():
if os.path.exists("/usr/local/cpanel/scripts/upcp"):
print("[*] Running /usr/local/cpanel/scripts/upcp --force ...")
run(["/usr/local/cpanel/scripts/upcp", "--force"], check=False)
def read_my_cnf_credentials():
creds = {}
if not os.path.exists(ROOT_MYCNF):
return creds
try:
with open(ROOT_MYCNF) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or line.startswith("["):
continue
if "=" in line:
k, v = line.split("=", 1)
k = k.strip().lower()
v = v.strip()
if k in ("user", "username"):
creds["user"] = v
elif k in ("password", "pass"):
creds["password"] = v
except Exception as e:
print(f"[!] Unable to read {ROOT_MYCNF}: {e}")
return creds
def write_root_mycnf(user, password):
"""Write /root/.my.cnf with provided credentials."""
try:
with open(ROOT_MYCNF, "w") as f:
f.write("[client]\n")
f.write(f"user={user}\n")
f.write(f"password={password}\n")
os.chmod(ROOT_MYCNF, 0o600)
print(f"[+] Wrote {ROOT_MYCNF} with provided credentials.")
return True
except Exception as e:
print(f"[!] Failed to write {ROOT_MYCNF}: {e}")
return False
def mysql_login_works():
mysql_bin = which("mysql")
if not mysql_bin or not os.path.exists(ROOT_MYCNF):
return False
proc = subprocess.run(
[mysql_bin, "--defaults-file=" + ROOT_MYCNF, "-e", "SELECT 1;"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return proc.returncode == 0
def fetch_temp_root_password():
log_path = os.path.join(DATADIR, f"{socket.gethostname()}.err")
if not os.path.exists(log_path):
return None
temp_pw = None
try:
with open(log_path) as f:
for line in f:
if "temporary password" in line.lower():
temp_pw = line.strip().split()[-1]
except Exception:
return None
return temp_pw
def ensure_root_password_matches_mycnf(max_wait=300, interval=5):
"""Ensure mysql root password matches credentials stored in /root/.my.cnf."""
# Wait for service to be reachable before auth attempts
print(
f"[*] Waiting up to {max_wait}s for database service to accept connections before root auth sync..."
)
if not wait_for_mysql_ready(
timeout=max_wait, interval=interval, require_auth=False
):
print("[!] Database service did not become reachable in time.")
return False
# Try a few times to handle slow starts
for attempt in range(1, 6):
if mysql_login_works():
return True
creds = read_my_cnf_credentials()
desired_user = creds.get("user", "root")
desired_pass = creds.get("password")
# If .my.cnf is missing or incomplete, attempt passwordless root login and write .my.cnf accordingly.
if desired_pass is None:
print(
f"[i] Attempt {attempt}: {ROOT_MYCNF} missing or lacks password; probing for passwordless root access..."
)
proc = subprocess.run(
["mysql", "-uroot", "-e", "SELECT 1;"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if proc.returncode == 0:
print(
"[+] Passwordless root login succeeded; writing /root/.my.cnf with empty password."
)
if write_root_mycnf("root", ""):
return True
else:
print(f"[!] Attempt {attempt}: passwordless root login failed.")
else:
temp_password = fetch_temp_root_password()
if temp_password:
print(
f"[*] Attempt {attempt}: synchronizing MySQL root password using temporary password."
)
env = os.environ.copy()
env["MYSQL_PWD"] = temp_password
sql = "ALTER USER 'root'@'localhost' IDENTIFIED BY '{}'".format(
desired_pass.replace("'", "\\'")
)
subprocess.run(
[
"mysql",
"-uroot",
"--connect-expired-password",
"-e",
sql,
],
env=env,
check=False,
)
if mysql_login_works():
print(
"[+] Root password synchronized with {}".format(
ROOT_MYCNF
)
)
return True
setpass = "/usr/local/cpanel/scripts/setmysqlrootpass"
if os.path.exists(setpass):
print(
f"[*] Attempt {attempt}: setting MySQL root password via setmysqlrootpass."
)
run([setpass, desired_pass], check=False)
if mysql_login_works():
print(
"[+] Root password synchronized with {}".format(
ROOT_MYCNF
)
)
return True
time.sleep(interval)
print(
"[!] Unable to synchronize MySQL root password automatically. Please ensure {} credentials are valid.".format(
ROOT_MYCNF
)
)
return False
def enable_mysql_module_80():
run(["dnf", "-y", "module", "reset", "mysql"], check=False)
run(["dnf", "-y", "module", "enable", "mysql:8.0"], check=False)
def install_mysql80():
# First, make sure we have a clean system
print("[*] Ensuring clean system state for MySQL 8.0 installation...")
# Stop any running database services
stop_db()
# Clean up any remaining packages and files
print("[*] Removing existing MySQL/MariaDB packages...")
# Remove all MySQL and MariaDB packages
existing_pkgs = rpm_packages_matching(r"^(mariadb|mysql)")
if existing_pkgs:
run(
["rpm", "-e", "--nodeps"] + existing_pkgs,
check=False,
destructive=True,
)
invalidate_rpm_cache()
# Clean up any remaining files
print("[*] Cleaning up remaining database files...")
remove_matches(
[
"/var/lib/mysql*",
"/etc/my.cnf*",
"/etc/mysql*",
]
)
# Remove any remaining packages that might conflict
conflicting = rpm_packages_matching(r"^(mariadb|mysql)", refresh=True)
if conflicting:
run(
["dnf", "-y", "remove"] + conflicting, check=False, destructive=True
)
invalidate_rpm_cache()
# Clean all dnf cache and metadata
print("[*] Cleaning package manager cache...")
run(["dnf", "clean", "all"], check=False)
remove_matches(["/var/cache/dnf"])
# Reset and enable MySQL 8.0 module
print("[*] Configuring MySQL 8.0 repository...")
run(["dnf", "-y", "module", "reset", "mysql"], check=False)
run(["dnf", "-y", "module", "disable", "mysql"], check=False)
# Remove any existing MySQL repositories to avoid conflicts
remove_matches(
[
"/etc/yum.repos.d/mariadb*.repo",
"/etc/yum.repos.d/mysql*.repo",
]
)
# Add MySQL 8.0 community repository
mysql_repo = """[mysql80-community]
name=MySQL 8.0 Community Server
baseurl=http://repo.mysql.com/yum/mysql-8.0-community/el/8/$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-mysql
"""
with open("/etc/yum.repos.d/mysql-community.repo", "w") as f:
f.write(mysql_repo)
# Import MySQL GPG key
run(
["rpm", "--import", "https://repo.mysql.com/RPM-GPG-KEY-mysql"],
check=False,
)
# Clean and rebuild the dnf cache
run(["dnf", "clean", "all"], check=False)
run(["dnf", "makecache"], check=False)
# Install MySQL 8.0 server with --allowerasing to handle any conflicts
print("[*] Installing MySQL 8.0 server...")
try:
# Install MySQL server and client
run(
[
"dnf",
"-y",
"install",
"mysql-community-server",
"--allowerasing",
],
check=True,
)
# For cPanel systems, create necessary symlinks
if os.path.exists("/usr/local/cpanel"):
print("[*] Configuring for cPanel compatibility...")
os.makedirs("/usr/local/mysql/bin", exist_ok=True)
for cmd in ["mysql", "mysqladmin", "mysqldump"]:
if os.path.exists(f"/usr/bin/{cmd}") and not os.path.exists(
f"/usr/local/mysql/bin/{cmd}"
):
os.symlink(f"/usr/bin/{cmd}", f"/usr/local/mysql/bin/{cmd}")
# Update cPanel MySQL version
with open("/var/cpanel/mysql/version", "w") as f:
f.write("8.0")
except Exception as e:
print(f"[!] Failed to install MySQL 8.0: {str(e)}")
print("[*] Attempting alternative installation method...")
# Try with module if direct installation fails
run(["dnf", "-y", "module", "reset", "mysql"], check=False)
run(["dnf", "-y", "module", "enable", "mysql:8.0"], check=False)
run(
["dnf", "-y", "install", "@mysql:8.0", "--allowerasing"], check=True
)
# Enable and start the service
print("[*] Starting MySQL 8.0 service...")
run(["systemctl", "daemon-reload"], check=True)
run(["systemctl", "enable", "--now", "mysqld"], check=True)
# Wait a moment for the service to start
time.sleep(5)
# Verify the service is running
if not ping_mysql():
print("[!] MySQL service failed to start. Checking status...")
run(["systemctl", "status", "mysqld"], check=False)
run(["journalctl", "-xe", "-u", "mysqld"], check=False)
# Try to get the error log
hostname_log = os.path.join(DATADIR, f"{socket.gethostname()}.err")
if os.path.exists(hostname_log):
print(f"\n[!] Error Log ({hostname_log} - last 20 lines):")
run(["tail", "-n", "20", hostname_log], check=False)
else:
print(f"[!] Expected error log {hostname_log} not found.")
print("\n[!] Trying to start MySQL manually...")
run(["mysqld", "--initialize-insecure", "--user=mysql"], check=False)
run(["systemctl", "start", "mysqld"], check=False)
if not ping_mysql():
raise Exception(
"Failed to start MySQL service after multiple attempts"
)
# Get temporary root password if this is a fresh install
temp_password = None
hostname_log = os.path.join(DATADIR, f"{socket.gethostname()}.err")
if os.path.exists(hostname_log):
with open(hostname_log) as f:
for line in f:
if "temporary password" in line.lower():
temp_password = line.strip().split()[-1]
break
# Secure the installation
print("[*] Securing MySQL installation...")
try:
if temp_password:
print(f"\n[!] IMPORTANT: Temporary root password: {temp_password}")
# Change root password and secure installation
cmds = [
"ALTER USER 'root'@'localhost' IDENTIFIED BY '';",
"DELETE FROM mysql.user WHERE User='';",
"DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');",
"DROP DATABASE IF EXISTS test;",
"DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';",
"FLUSH PRIVILEGES;",
]
# Execute security commands
for cmd in cmds:
run(
[
"mysql",
"--user=root",
f"--password={temp_password}",
"--connect-expired-password",
"-e",
cmd,
],
check=False,
)
else:
print(
"[*] No temporary password found, assuming existing installation"
)
except Exception as e:
print(f"[!] Warning: Could not secure installation: {str(e)}")
# For cPanel systems, update configuration
if os.path.exists("/usr/local/cpanel"):
print("[*] Updating cPanel configuration...")
cpanel_scripts = [
"/usr/local/cpanel/bin/update_local_rpm_versions",
"/usr/local/cpanel/bin/check-db-stable-version",
"/usr/local/cpanel/scripts/check_cpanel_mysql_routines",
]
for script in cpanel_scripts:
if os.path.exists(script):
try:
run([script], check=False)
except Exception as e:
print(f"[!] Warning: Failed to run {script}: {str(e)}")
# Run upcp to restore cPanel users and databases
print(
"[*] Running cPanel update (upcp) to restore users and databases..."
)
print(" This may take several minutes to complete...")
run(["/usr/local/cpanel/scripts/upcp", "--force"], check=False)
print("\n[+] MySQL 8.0 installation completed successfully!")
print("\n[!] IMPORTANT NEXT STEPS:")
if temp_password:
print(
" 1. Set a root password using: mysqladmin -u root -p password 'new-password'"
)
else:
print(
" 1. If you need to reset the root password, use: mysql_secure_installation"
)
print(" 2. Verify all databases and users are accessible")
print(" 3. Check application compatibility with MySQL 8.0")
print("\n[!] If you encounter any issues, check these log files:")
print(" - /var/lib/mysql/*.*.err")
print(" - journalctl -xe | grep -i mysql")
def install_mariadb106():
print("[*] Installing MariaDB 10.6...")
# First, ensure we have a clean system
print("[*] Ensuring clean system state for MariaDB 10.6 installation...")
remove_mysql_family() # Make sure MySQL is completely removed
# Add MariaDB 10.6 repository
print("[*] Setting up MariaDB 10.6 repository...")
maria_repo = """[mariadb]
name = MariaDB
baseurl = http://yum.mariadb.org/10.6/rhel8-amd64
module_hotfixes=1
gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
gpgcheck=1
"""
with open("/etc/yum.repos.d/mariadb.repo", "w") as f:
f.write(maria_repo)
# Import GPG key
run(
["rpm", "--import", "https://yum.mariadb.org/RPM-GPG-KEY-MariaDB"],
check=False,
)
# Install required dependencies first
print("[*] Installing required dependencies...")
run(
[
"dnf",
"-y",
"install",
"socat",
"libnsl",
"perl-DBD-MySQL",
"policycoreutils-python-utils",
],
check=True,
)
# Clean DNF cache
run(["dnf", "clean", "all"], check=False)
run(["dnf", "makecache"], check=False)
# Install MariaDB 10.6 server
print("[*] Installing MariaDB 10.6 server...")
try:
# Install base MariaDB packages
run(
[
"dnf",
"-y",
"install",
"MariaDB-server",
"MariaDB-client",
"MariaDB-shared",
"MariaDB-common",
"MariaDB-compat",
],
check=True,
)
# Install cPanel specific packages if cPanel is detected
if os.path.exists("/usr/local/cpanel"):
print("[*] Configuring for cPanel compatibility...")
# Create necessary symlinks for cPanel
for cmd in ["mysql", "mysqladmin", "mysqldump"]:
if os.path.exists(f"/usr/bin/{cmd}") and not os.path.exists(
f"/usr/local/mysql/bin/{cmd}"
):
os.makedirs("/usr/local/mysql/bin", exist_ok=True)
os.symlink(f"/usr/bin/{cmd}", f"/usr/local/mysql/bin/{cmd}")
# Update cPanel MySQL version
with open("/var/cpanel/mysql/version", "w") as f:
f.write("10.6")
except Exception as e:
print(f"[!] Warning during MariaDB installation: {str(e)}")
# Configure SELinux if it's enabled
if os.path.exists("/usr/sbin/sestatus"):
print("[*] Configuring SELinux for MariaDB...")
try:
# Set proper SELinux contexts
run(
[
"semanage",
"port",
"-a",
"-t",
"mysqld_port_t",
"-p",
"tcp",
"3306",
],
check=False,
)
run(
[
"semanage",
"port",
"-m",
"-t",
"mysqld_port_t",
"-p",
"tcp",
"3306",
],
check=False,
)
# Set file contexts for MariaDB
run(
[
"semanage",
"fcontext",
"-a",
"-t",
"mysqld_db_t",
"/var/lib/mysql(/.*)?",
],
check=False,
)
run(["restorecon", "-R", "/var/lib/mysql"], check=False)
except Exception as e:
print(f"[!] Warning: Could not configure SELinux: {str(e)}")
# Enable and start the service
print("[*] Starting MariaDB service...")
run(["systemctl", "daemon-reload"], check=True)
run(["systemctl", "enable", "--now", "mariadb"], check=True)
# Verify the service is running
if not ping_mysql():
print("[!] MariaDB service failed to start. Checking status...")
run(["systemctl", "status", "mariadb"], check=False)
run(["journalctl", "-xe", "-u", "mariadb"], check=False)
# Try to get the error log
error_logs = [
"/var/lig/mysql/*.*.err",
f"/var/lib/mysql/{os.uname().nodename}.err",
"/var/lib/mysql/*.*.err",
]
for log in error_logs:
if os.path.exists(log):
print(f"\n[!] Error Log ({log} - last 20 lines):")
run(["tail", "-n", "20", log], check=False)
print("\n[!] Trying to start MariaDB with systemd...")
run(["systemctl", "start", "mariadb"], check=False)
if not ping_mysql():
print("\n[!] Attempting to start MariaDB directly...")
run_background(
[
"/usr/libexec/mysqld",
"--skip-grant-tables",
"--skip-networking",
]
)
# Final verification
if not ping_mysql():
print(
"\n[!] ERROR: Could not start MariaDB. Please check the error logs above."
)
print(
" You may need to manually configure MariaDB or check for port conflicts."
)
print(" Try running: journalctl -xe | grep -i mysql")
return False
# Secure the installation
print("[*] Securing MariaDB installation...")
try:
# Set root password and secure installation
cmds = [
"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('');",
"DELETE FROM mysql.user WHERE User='';",
"DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');",
"DROP DATABASE IF EXISTS test;",
"DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';",
"FLUSH PRIVILEGES;",
]
# Execute security commands
for cmd in cmds:
try:
run(["mysql", "-e", cmd], check=False)
except Exception as e:
print(f"[!] Warning: Failed to execute: {cmd}")
print(f" Error: {str(e)}")
# For cPanel systems, update the configuration
if os.path.exists("/usr/local/cpanel"):
print("[*] Updating cPanel MySQL configuration...")
cpanel_scripts = [
"/usr/local/cpanel/bin/update_local_rpm_versions",
"/usr/local/cpanel/bin/check-db-stable-version",
"/usr/local/cpanel/scripts/check_cpanel_mysql_routines",
]
for script in cpanel_scripts:
if os.path.exists(script):
try:
run([script], check=False)
except Exception as e:
print(f"[!] Warning: Failed to run {script}: {str(e)}")
# Restart cPanel services and run upcp if cPanel is installed
if os.path.exists("/usr/local/cpanel"):
print("[*] Restarting cPanel services...")
run(["/usr/local/cpanel/scripts/restartsrv_cpsrvd"], check=False)
# Run upcp to restore cPanel users and databases
print(
"[*] Running cPanel update (upcp) to restore users and databases..."
)
print(" This may take several minutes to complete...")
run(["/usr/local/cpanel/scripts/upcp", "--force"], check=False)
except Exception as e:
print(f"[!] Warning: Could not complete all security steps: {str(e)}")
print("\n[+] MariaDB 10.6 installation completed successfully!")
print("\n[!] IMPORTANT NEXT STEPS:")
print(
" 1. Set a root password using: mysqladmin -u root password 'your-new-password'"
)
print(
" 2. If using cPanel, run: /usr/local/cpanel/bin/update_local_rpm_versions"
)
print(" 3. Check MySQL users and permissions")
print("\n[!] If you encounter any issues, check these log files:")
print(" - /var/lib/mysql/*.*.err")
print(" - journalctl -xe | grep -i mysql")
def discover_cpusers():
"""Return a sorted list of cPanel users inferred from DB metadata and user files."""
users = set()
meta_dir = "/var/cpanel/databases"
users_dir = "/var/cpanel/users"
if os.path.isdir(meta_dir):
for entry in os.listdir(meta_dir):
if entry.endswith(".yaml"):
users.add(entry[:-5])
if os.path.isdir(users_dir):
for entry in os.listdir(users_dir):
if re.match(r"^[A-Za-z0-9_]+$", entry):
users.add(entry)
return sorted(users)
def restoregrants_all_users():
"""Restore grants for each cPanel user, aligning with cPanel guidance."""
script = "/usr/local/cpanel/bin/restoregrants"
if not os.path.exists(script):
print("[!] restoregrants script not found; skipping grant restoration.")
return False
cpusers = discover_cpusers()
if not cpusers:
print("[*] No cPanel users detected for grant restoration.")
return True
ok = True
for user in cpusers:
rc = run([script, f"--cpuser={user}"], check=False)
if rc != 0:
print(f"[!] restoregrants failed for cpuser {user}")
ok = False
return ok
def restart_mysql_service():
"""Restart MySQL/MariaDB service to pick up restored settings."""
print("[*] Restarting database service...")
# Try common service names; ignore failures to avoid aborting flow.
run(["systemctl", "restart", "mysqld"], check=False)
run(["systemctl", "restart", "mysql"], check=False)
run(["systemctl", "restart", "mariadb"], check=False)
def post_restore_health_check():
"""Basic service + data sanity check after restore."""
if DRY_RUN:
print("[dry-run] Skipping post-restore health check.")
return True
mysql_bin = which("mysql")
if not mysql_bin:
print("[!] mysql client not found; skipping post-restore health check.")
return False
if not wait_for_mysql_ready(timeout=180, interval=5, require_auth=True):
print("[!] MySQL/MariaDB did not become ready for health check.")
return False
dbs_raw = out(
[mysql_bin, "--defaults-file=" + ROOT_MYCNF, "-NBe", "SHOW DATABASES"]
)
dbs = [d.strip() for d in dbs_raw.splitlines() if d.strip()]
system_dbs = {"mysql", "information_schema", "performance_schema", "sys"}
user_dbs = [d for d in dbs if d not in system_dbs]
print(f"[*] Health check: found {len(user_dbs)} user databases.")
if not user_dbs:
return True
sample_db = user_dbs[0]
tables_raw = out(
[
mysql_bin,
"--defaults-file=" + ROOT_MYCNF,
"-NBe",
f"SHOW TABLES IN `{sample_db}`",
]
)
tables = [t.strip() for t in tables_raw.splitlines() if t.strip()]
if not tables:
print(
f"[i] Health check: sample DB {sample_db} has no tables to check."
)
return True
sample_table = tables[0]
rc = run(
[
mysql_bin,
"--defaults-file=" + ROOT_MYCNF,
"-e",
f"CHECK TABLE `{sample_db}`.`{sample_table}`",
],
check=False,
)
if rc != 0:
print(f"[!] Health check failed on {sample_db}.{sample_table}")
return False
print(f"[+] Health check passed on {sample_db}.{sample_table}")
return True
def get_current_mariadb_version():
try:
matches = rpm_packages_matching(r"^mariadb-server")
if not matches:
return None
# Extract version number from package name (e.g., 'mariadb-server-10.11.5-1.el9_2.x86_64' -> '10.11')
match = re.search(r'mariadb[^\d]*(\d+\.\d+)', matches[0])
if match:
return match.group(1)
except Exception as e:
print(f"[!] Could not determine current MariaDB version: {e}")
return None
def needs_downgrade(current_ver, target_ver):
if not current_ver or not target_ver:
return False
try:
return float(current_ver) > float(target_ver)
except (ValueError, TypeError):
return False
def main():
global DRY_RUN, FORCE_DELETE
args = sys.argv[1:]
positional = []
log_path = None
for arg in args:
if arg in ("-h", "--help"):
init_logging(log_path)
print_help(sys.argv[0])
sys.exit(0)
elif arg == "--dry-run":
DRY_RUN = True
elif arg == "--force":
FORCE_DELETE = True
elif arg.startswith("--log-file="):
log_path = arg.split("=", 1)[1]
else:
positional.append(arg)
init_logging(log_path)
print(f"[*] Logging to: {LOG_FILE_DEFAULT}")
if not positional:
print_help(sys.argv[0])
sys.exit(1)
check_os_support()
target_key = positional[0]
if target_key not in TARGETS:
print(
"[!] Invalid target: {}\n Allowed: {}".format(
target_key, ", ".join(TARGETS.keys())
)
)
sys.exit(1)
tfamily, tver = TARGETS[target_key]
current_family = installed_family()
current_ver = None
if current_family == "mariadb":
current_ver = get_current_mariadb_version()
elif current_family == "mysql":
current_ver = get_current_mysql_version()
if current_family == tfamily and current_ver == tver:
print(
f"[*] {tfamily.title()} {current_ver} is already installed. No changes needed."
)
sys.exit(0)
switching = current_family not in ("none", tfamily)
downgrading = (current_family == tfamily) and needs_downgrade(
current_ver, tver
)
print(f"Target: {target_key} ({tver})")
print(f"When: {ts()}")
# 1. First backup all databases while the server is still running
print("\n[1/5] Backing up all user databases...")
if not has_sufficient_space_for_backup():
print("[!] Aborting due to insufficient space for backups.")
sys.exit(1)
backup_dir, backup_status = backup_all(BACKUP_ROOT)
if backup_status == "error":
print(
"[!] Backup failed; aborting to prevent data loss. Backup directory: {}".format(
backup_dir
)
)
sys.exit(1)
if backup_status == "empty":
ans = (
input(
"[?] No user databases were found to back up. Continue anyway? [y/N]: "
)
.strip()
.lower()
)
if ans not in ("y", "yes"):
print("[!] Aborting at user request due to missing backups.")
sys.exit(1)
# 2. Stop database services
print("\n[2/5] Stopping database services...")
stop_db()
# 3. Remove existing installation
print("\n[3/5] Removing existing installation (if any)...")
if current_family == 'mariadb':
print("[*] Removing existing MariaDB installation...")
remove_mariadb_family()
elif current_family == 'mysql':
print("[*] Removing existing MySQL installation...")
remove_mysql_family()
else:
print("[*] No existing MySQL/MariaDB packages detected.")
# 4. Clean up any remaining packages
print("\n[4/5] Cleaning up...")
run(["dnf", "-y", "autoremove"], check=False, destructive=True)
# Ensure Perl modules after cleanup, before installing new DB version
print("[*] Ensuring Perl MySQL modules are present...")
ensure_perl_mysql_modules()
# Set the desired version in cPanel config
set_cpanel_mysql_version(tver)
# Check what WHM allows (informational only)
allowed = whm_allowed()
if allowed:
print("[i] WHM (probe) allows: {}".format(", ".join(allowed)))
# Use cPanel API to install requested version
print("\n[5/5] Installing requested engine via cPanel API...")
install_ok = install_with_cpanel_api(tver)
# Fallbacks if API is not available or fails
if not install_ok:
if tfamily == "mariadb" and tver == "10.6":
print(
"\n[!] API install failed; falling back to manual MariaDB {} install...".format(
tver
)
)
install_mariadb106()
install_ok = True
elif tfamily == "mysql" and tver == "8.0":
print(
"\n[!] API install failed; falling back to manual MySQL {} install...".format(
tver
)
)
install_mysql80()
install_ok = True
else:
print(f"[!] No manual fallback implemented for {tfamily} {tver}.")
if not install_ok:
print(
"\n[!] Installation did not complete successfully for {}.".format(
tver
)
)
print(f" - Backup was saved to: {backup_dir}")
print(
" - Track WHM status with: whmapi1 get_mysql_upgrade --output=json|yaml"
)
sys.exit(1)
# Ensure /root/.my.cnf credentials work with the new service
if not ensure_root_password_matches_mycnf():
print(
"[!] Unable to authenticate with MySQL using /root/.my.cnf credentials. Aborting to prevent data issues."
)
sys.exit(1)
# Restore data and finish up
if not restore_all_databases(backup_dir):
print(
"[!] One or more database restores failed; check logs before proceeding."
)
sys.exit(1)
restore_max_allowed_packet()
restart_mysql_service()
if not restoregrants_all_users():
print(
"[!] One or more grant restoration steps failed. Review output above."
)
run_upcp_force()
health_ok = post_restore_health_check()
if not health_ok:
print(
"[!] Post-restore health check reported issues. Review logs and database state."
)
else:
print("[+] Post-restore health check passed.")
print("[+] Requested database engine installed and data restore complete.")
print(f" - Backup directory retained at: {backup_dir}")
if __name__ == "__main__":
main()
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists