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.
This commit is contained in:
usmannasir
2025-08-23 16:02:19 +05:00
parent 232014a4cb
commit cd9792c941
2 changed files with 347 additions and 1 deletions

View File

@@ -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

View File

@@ -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