From cd9792c941747431fe0edd31f47aa9a6c555c774 Mon Sep 17 00:00:00 2001 From: usmannasir Date: Sat, 23 Aug 2025 16:02:19 +0500 Subject: [PATCH] feat: Add comprehensive recovery mechanism for damaged CyberPanel installations during upgrade - Implement directory integrity checks to detect missing CyberPanel components - Add automatic recovery by cloning fresh repository when essential directories are lost - Create database credential recovery with automatic password reset capability - Update all service configurations (FTP, PowerDNS, Postfix, Dovecot) when password is reset - Add service restart functionality to apply new configurations - Preserve existing configuration files during recovery process - Handle both Ubuntu (root password) and CentOS (separate password) database configurations This ensures upgrades can complete successfully even when /usr/local/CyberCP is completely lost. --- cyberpanel_upgrade.sh | 73 +++++++++++ plogical/upgrade.py | 275 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 347 insertions(+), 1 deletion(-) diff --git a/cyberpanel_upgrade.sh b/cyberpanel_upgrade.sh index e3ceff9c0..d30b5945d 100644 --- a/cyberpanel_upgrade.sh +++ b/cyberpanel_upgrade.sh @@ -575,6 +575,79 @@ done Pre_Upgrade_Required_Components() { +# Check if CyberCP directory exists but is incomplete/damaged +echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checking CyberCP directory integrity..." | tee -a /var/log/cyberpanel_upgrade_debug.log + +# Define essential CyberCP components +CYBERCP_ESSENTIAL_DIRS=( + "/usr/local/CyberCP/CyberCP" + "/usr/local/CyberCP/plogical" + "/usr/local/CyberCP/websiteFunctions" + "/usr/local/CyberCP/manage" +) + +CYBERCP_MISSING=0 +for dir in "${CYBERCP_ESSENTIAL_DIRS[@]}"; do + if [ ! -d "$dir" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] WARNING: Essential directory missing: $dir" | tee -a /var/log/cyberpanel_upgrade_debug.log + CYBERCP_MISSING=1 + fi +done + +# If essential directories are missing, perform recovery +if [ $CYBERCP_MISSING -eq 1 ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] RECOVERY: CyberCP installation appears damaged or incomplete. Initiating recovery..." | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Backup any remaining configuration files if they exist + if [ -f "/usr/local/CyberCP/CyberCP/settings.py" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Backing up existing settings.py..." | tee -a /var/log/cyberpanel_upgrade_debug.log + cp /usr/local/CyberCP/CyberCP/settings.py /tmp/cyberpanel_settings_backup.py + fi + + # Clone fresh CyberPanel repository + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Cloning fresh CyberPanel repository for recovery..." | tee -a /var/log/cyberpanel_upgrade_debug.log + cd /usr/local + rm -rf CyberCP_recovery_tmp + + if git clone https://github.com/usmannasir/cyberpanel CyberCP_recovery_tmp; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Repository cloned successfully for recovery" | tee -a /var/log/cyberpanel_upgrade_debug.log + + # Checkout the appropriate branch + cd CyberCP_recovery_tmp + git checkout "$Branch_Name" 2>/dev/null || git checkout stable + + # Copy missing components while preserving existing configurations + for dir in "${CYBERCP_ESSENTIAL_DIRS[@]}"; do + if [ ! -d "$dir" ]; then + # Extract relative path after /usr/local/CyberCP/ + relative_path=${dir#/usr/local/CyberCP/} + if [ -d "/usr/local/CyberCP_recovery_tmp/$relative_path" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restoring missing directory: $dir" | tee -a /var/log/cyberpanel_upgrade_debug.log + mkdir -p "$(dirname "$dir")" + cp -r "/usr/local/CyberCP_recovery_tmp/$relative_path" "$dir" + fi + fi + done + + # Restore settings.py if it was backed up + if [ -f "/tmp/cyberpanel_settings_backup.py" ]; then + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Restoring backed up settings.py..." | tee -a /var/log/cyberpanel_upgrade_debug.log + cp /tmp/cyberpanel_settings_backup.py /usr/local/CyberCP/CyberCP/settings.py + fi + + # Clean up temporary clone + rm -rf /usr/local/CyberCP_recovery_tmp + + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Recovery completed. CyberCP structure restored." | tee -a /var/log/cyberpanel_upgrade_debug.log + else + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] ERROR: Failed to clone repository for recovery" | tee -a /var/log/cyberpanel_upgrade_debug.log + echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Please run full installation instead of upgrade" | tee -a /var/log/cyberpanel_upgrade_debug.log + exit 1 + fi + + cd /root/cyberpanel_upgrade_tmp || cd /root +fi + if [ "$Server_OS" = "Ubuntu" ]; then echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Preparing Ubuntu environment for virtualenv..." | tee -a /var/log/cyberpanel_upgrade_debug.log rm -rf /usr/local/CyberPanel diff --git a/plogical/upgrade.py b/plogical/upgrade.py index c447a3112..99ed49444 100644 --- a/plogical/upgrade.py +++ b/plogical/upgrade.py @@ -5,6 +5,7 @@ import sys import argparse import pwd import grp +import re sys.path.append('/usr/local/CyberCP') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "CyberCP.settings") @@ -13,10 +14,282 @@ import subprocess import shutil import time import MySQLdb as mysql -from CyberCP import settings import random import string +def update_all_config_files_with_password(new_password): + """ + Update all configuration files that use the cyberpanel database password. + This includes FTP, PowerDNS, Postfix, Dovecot configurations. + """ + config_updates = [ + # Django settings + { + 'path': '/usr/local/CyberCP/CyberCP/settings.py', + 'updates': [ + (r"('cyberpanel'[^}]+?'PASSWORD':\s*')[^']+'", r"\1%s'" % new_password) + ] + }, + # FTP configurations + { + 'path': '/etc/pure-ftpd/pureftpd-mysql.conf', + 'updates': [ + (r'^MYSQLPassword\s+.*$', 'MYSQLPassword %s' % new_password) + ] + }, + { + 'path': '/etc/pure-ftpd/db/mysql.conf', # Ubuntu specific + 'updates': [ + (r'^MYSQLPassword\s+.*$', 'MYSQLPassword %s' % new_password) + ] + }, + # PowerDNS configurations + { + 'path': '/etc/pdns/pdns.conf', # CentOS/RHEL + 'updates': [ + (r'^gmysql-password=.*$', 'gmysql-password=%s' % new_password) + ] + }, + { + 'path': '/etc/powerdns/pdns.conf', # Ubuntu/Debian + 'updates': [ + (r'^gmysql-password=.*$', 'gmysql-password=%s' % new_password) + ] + }, + # Postfix MySQL configurations + { + 'path': '/etc/postfix/mysql-virtual_domains.cf', + 'updates': [ + (r'^password\s*=.*$', 'password = %s' % new_password) + ] + }, + { + 'path': '/etc/postfix/mysql-virtual_forwardings.cf', + 'updates': [ + (r'^password\s*=.*$', 'password = %s' % new_password) + ] + }, + { + 'path': '/etc/postfix/mysql-virtual_mailboxes.cf', + 'updates': [ + (r'^password\s*=.*$', 'password = %s' % new_password) + ] + }, + { + 'path': '/etc/postfix/mysql-virtual_email2email.cf', + 'updates': [ + (r'^password\s*=.*$', 'password = %s' % new_password) + ] + }, + # Dovecot MySQL configuration + { + 'path': '/etc/dovecot/dovecot-sql.conf.ext', + 'updates': [ + (r'^connect\s*=.*$', lambda m: update_dovecot_connect_string(m.group(0), new_password)) + ] + } + ] + + for config in config_updates: + if not os.path.exists(config['path']): + continue + + try: + with open(config['path'], 'r') as f: + content = f.read() + + original_content = content + for pattern, replacement in config['updates']: + if callable(replacement): + # For complex replacements like dovecot connect string + content = re.sub(pattern, replacement, content, flags=re.MULTILINE) + else: + content = re.sub(pattern, replacement, content, flags=re.MULTILINE) + + if content != original_content: + with open(config['path'], 'w') as f: + f.write(content) + print("[RECOVERY] Updated password in: %s" % config['path']) + except Exception as e: + print("[RECOVERY] Warning: Could not update %s: %s" % (config['path'], str(e))) + +def update_dovecot_connect_string(connect_line, new_password): + """ + Update the password in dovecot's connect string. + Format: connect = host=localhost dbname=cyberpanel user=cyberpanel password=oldpass + """ + # Replace the password part in the connect string + updated = re.sub(r'password=\S+', 'password=%s' % new_password, connect_line) + return updated + +def restart_affected_services(): + """ + Restart services that use the cyberpanel database password. + """ + services_to_restart = [ + 'pure-ftpd', # FTP service + 'postfix', # Mail transfer agent + 'dovecot', # IMAP/POP3 server + 'pdns', # PowerDNS (CentOS/RHEL) + 'powerdns', # PowerDNS (Ubuntu/Debian) + ] + + for service in services_to_restart: + try: + # Try systemctl first (systemd) + result = subprocess.run(['systemctl', 'restart', service], + capture_output=True, text=True) + if result.returncode == 0: + print("[RECOVERY] Restarted service: %s" % service) + elif 'Unit' in result.stderr and 'not found' in result.stderr: + # Service doesn't exist, skip + pass + else: + # Try service command (older systems) + result = subprocess.run(['service', service, 'restart'], + capture_output=True, text=True) + if result.returncode == 0: + print("[RECOVERY] Restarted service: %s" % service) + except Exception as e: + print("[RECOVERY] Warning: Could not restart %s: %s" % (service, str(e))) + +# Try to import settings, but handle case where CyberCP directory is damaged +try: + from CyberCP import settings +except ImportError: + print("WARNING: Cannot import CyberCP settings. Attempting recovery...") + + def recover_database_credentials(): + """Attempt to recover or reset database credentials""" + + # First, ensure we have root MySQL password + if not os.path.exists('/etc/cyberpanel/mysqlPassword'): + print("FATAL: Cannot find MySQL root password file at /etc/cyberpanel/mysqlPassword") + print("Manual intervention required.") + sys.exit(1) + + root_password = open('/etc/cyberpanel/mysqlPassword', 'r').read().strip() + cyberpanel_password = None + + # Try to read existing settings.py to get cyberpanel password + settings_path = '/usr/local/CyberCP/CyberCP/settings.py' + if os.path.exists(settings_path): + try: + with open(settings_path, 'r') as f: + settings_content = f.read() + + import re + # Extract cyberpanel database password + db_pattern = r"'default':[^}]*'USER':\s*'cyberpanel'[^}]*'PASSWORD':\s*'([^']+)'" + match = re.search(db_pattern, settings_content, re.DOTALL) + + if match: + cyberpanel_password = match.group(1) + print("Found existing cyberpanel password in settings.py") + + # Test if this password actually works + try: + test_conn = mysql.connect(host='localhost', user='cyberpanel', + passwd=cyberpanel_password, db='cyberpanel') + test_conn.close() + print("Verified cyberpanel database credentials are valid") + except: + print("Found password in settings.py but it doesn't work, will reset") + cyberpanel_password = None + except Exception as e: + print("Could not extract password from settings.py: %s" % str(e)) + + # If we couldn't get a working password, we need to reset it + if cyberpanel_password is None: + print("Resetting cyberpanel database user password...") + + # Check if we're on Ubuntu or CentOS + # On Ubuntu, cyberpanel uses root password; on CentOS, it uses a separate password + if os.path.exists('/etc/lsb-release'): + # Ubuntu - use root password + cyberpanel_password = root_password + reset_to_root = True + else: + # CentOS/others - generate new password + chars = string.ascii_letters + string.digits + cyberpanel_password = ''.join(random.choice(chars) for _ in range(14)) + reset_to_root = False + + try: + # Connect as root and reset cyberpanel user + conn = mysql.connect(host='localhost', user='root', passwd=root_password) + cursor = conn.cursor() + + # Check if cyberpanel database exists + cursor.execute("SHOW DATABASES LIKE 'cyberpanel'") + if not cursor.fetchone(): + print("Creating cyberpanel database...") + cursor.execute("CREATE DATABASE IF NOT EXISTS cyberpanel") + + # Reset cyberpanel user - drop and recreate to ensure clean state + cursor.execute("DROP USER IF EXISTS 'cyberpanel'@'localhost'") + cursor.execute("CREATE USER 'cyberpanel'@'localhost' IDENTIFIED BY '%s'" % cyberpanel_password) + cursor.execute("GRANT ALL PRIVILEGES ON cyberpanel.* TO 'cyberpanel'@'localhost'") + cursor.execute("FLUSH PRIVILEGES") + + conn.close() + + if reset_to_root: + print("Reset cyberpanel user password to match root password (Ubuntu style)") + else: + print("Reset cyberpanel user with new generated password (CentOS style)") + + # Update all configuration files with the new password + print("Updating all service configuration files with new password...") + update_all_config_files_with_password(cyberpanel_password) + + # Restart affected services to pick up new configuration + print("Restarting affected services...") + restart_affected_services() + + # Save the password to a temporary file for the upgrade process + temp_pass_file = '/tmp/cyberpanel_recovered_password' + with open(temp_pass_file, 'w') as f: + f.write(cyberpanel_password) + os.chmod(temp_pass_file, 0o600) + print("Saved recovered password to temporary file") + + except Exception as e: + print("Failed to reset cyberpanel database user: %s" % str(e)) + print("Manual intervention required. Please run:") + print(" mysql -u root -p") + print(" CREATE DATABASE IF NOT EXISTS cyberpanel;") + print(" GRANT ALL PRIVILEGES ON cyberpanel.* TO 'cyberpanel'@'localhost' IDENTIFIED BY 'your_password';") + print(" FLUSH PRIVILEGES;") + sys.exit(1) + + return cyberpanel_password, root_password + + # Perform recovery + cyberpanel_password, root_password = recover_database_credentials() + + # Create a minimal settings object for recovery + class MinimalSettings: + DATABASES = { + 'default': { + 'NAME': 'cyberpanel', + 'USER': 'cyberpanel', + 'PASSWORD': cyberpanel_password, + 'HOST': 'localhost', + 'PORT': '3306' + }, + 'rootdb': { + 'NAME': 'mysql', + 'USER': 'root', + 'PASSWORD': root_password, + 'HOST': 'localhost', + 'PORT': '3306' + } + } + + settings = MinimalSettings() + print("Recovery complete. Continuing with upgrade...") + VERSION = '2.4' BUILD = 3