mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
1074 lines
37 KiB
Bash
1074 lines
37 KiB
Bash
#!/bin/bash
|
|
pid=$$
|
|
script_dir=$(dirname "$0")
|
|
timestamp="$(date +'%Y-%m-%d_%H-%M-%S')" #used by log file name
|
|
start_time=$(date +%s) #used to calculate elapsed time at the end
|
|
|
|
set -eo pipefail
|
|
|
|
###############################################################
|
|
# HELPER FUNCTIONS
|
|
|
|
usage() {
|
|
echo "Usage: $0 --backup-location <path> --plan-name <plan_name> [--dry-run]"
|
|
echo ""
|
|
echo "Example: $0 --backup-location /home/backup-7.29.2024_13-22-32_stefan.tar.gz --plan-name default_plan_nginx --dry-run"
|
|
exit 1
|
|
}
|
|
|
|
log() {
|
|
local message="$1"
|
|
local timestamp=$(date +'%Y-%m-%d %H:%M:%S')
|
|
echo "[$timestamp] $message" | tee -a "$log_file"
|
|
}
|
|
|
|
DEBUG=true # Set this to true to enable debug logging
|
|
|
|
debug_log() {
|
|
if [ "$DEBUG" = true ]; then
|
|
log "DEBUG: $1"
|
|
fi
|
|
}
|
|
handle_error() {
|
|
log "FATAL ERROR: An error occurred in function '$1' on line $2"
|
|
cleanup
|
|
exit 1
|
|
}
|
|
|
|
trap 'handle_error "${FUNCNAME[-1]}" "$LINENO"' ERR
|
|
|
|
cleanup() {
|
|
log "Cleaning up temporary files and directories"
|
|
rm -rf "$backup_dir"
|
|
}
|
|
|
|
define_data_and_log(){
|
|
local backup_location=""
|
|
local plan_name=""
|
|
DRY_RUN=false
|
|
|
|
# Parse command-line arguments
|
|
while [ "$1" != "" ]; do
|
|
case $1 in
|
|
--backup-location ) shift
|
|
backup_location=$1
|
|
;;
|
|
--plan-name ) shift
|
|
plan_name=$1
|
|
;;
|
|
--dry-run ) DRY_RUN=true
|
|
;;
|
|
--post-hook ) shift
|
|
post_hook=$1
|
|
;;
|
|
* ) usage
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Validate required parameters
|
|
if [ -z "$backup_location" ] || [ -z "$plan_name" ]; then
|
|
usage
|
|
fi
|
|
|
|
# Format log file
|
|
base_name="$(basename "$backup_location")"
|
|
base_name_no_ext="${base_name%.*}"
|
|
local log_dir="/var/log/openpanel/admin/imports"
|
|
mkdir -p $log_dir
|
|
log_file="$log_dir/${base_name_no_ext}_${timestamp}.log"
|
|
|
|
# Run the main function
|
|
echo "Import started, log file: $log_file"
|
|
|
|
main
|
|
}
|
|
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
install_dependencies() {
|
|
log "Checking and installing dependencies..."
|
|
|
|
install_needed=false
|
|
|
|
# needed commands
|
|
declare -A commands=(
|
|
["tar"]="tar"
|
|
["parallel"]="parallel"
|
|
["rsync"]="rsync"
|
|
["unzip"]="unzip"
|
|
["jq"]="jq"
|
|
["mysql"]="mysql-client"
|
|
["wget"]="wget"
|
|
["curl"]="curl"
|
|
)
|
|
|
|
for cmd in "${!commands[@]}"; do
|
|
if ! command_exists "$cmd"; then
|
|
install_needed=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
# If installation is needed, update package list and install missing packages
|
|
if [ "$install_needed" = true ]; then
|
|
log "Updating package manager..."
|
|
|
|
# Hold kernel packages to prevent upgrades
|
|
sudo apt-mark hold linux-image-generic linux-headers-generic
|
|
|
|
# Update package list without upgrading
|
|
sudo apt-get update -y >/dev/null 2>&1
|
|
|
|
for cmd in "${!commands[@]}"; do
|
|
if ! command_exists "$cmd"; then
|
|
log "Installing ${commands[$cmd]}"
|
|
# Install package without upgrading or installing recommended packages
|
|
sudo apt-get install -y --no-upgrade --no-install-recommends "${commands[$cmd]}" >/dev/null 2>&1
|
|
fi
|
|
done
|
|
|
|
# Unhold kernel packages
|
|
sudo apt-mark unhold linux-image-generic linux-headers-generic
|
|
|
|
log "Dependencies installed successfully."
|
|
else
|
|
log "All required dependencies are already installed."
|
|
fi
|
|
}
|
|
get_server_ipv4(){
|
|
# Get server ipv4 from ip.openpanel.co or ifconfig.me
|
|
new_ip=$(curl --silent --max-time 2 -4 https://ip.openpanel.co || wget --timeout=2 -qO- https://ip.openpanel.co || curl --silent --max-time 2 -4 https://ifconfig.me)
|
|
|
|
# if no internet, get the ipv4 from the hostname -I
|
|
if [ -z "$new_ip" ]; then
|
|
new_ip=$(ip addr|grep 'inet '|grep global|head -n1|awk '{print $2}'|cut -f1 -d/)
|
|
fi
|
|
}
|
|
|
|
validate_plan_exists(){
|
|
if ! opencli plan-list --json | grep -qw "$plan_name"; then
|
|
log "FATAL ERROR: Plan name '$plan_name' does not exist."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
###############################################################
|
|
# MAIN FUNCTIONS
|
|
|
|
# CHECK EXTENSION AND DETERMINE SIZE
|
|
check_if_valid_cp_backup(){
|
|
local backup_location="$1"
|
|
|
|
ARCHIVE_SIZE=$(stat -c%s "$backup_location")
|
|
|
|
# Identify the backup type
|
|
local backup_filename=$(basename "$backup_location")
|
|
extraction_command=""
|
|
|
|
case "$backup_filename" in
|
|
cpmove-*.tar.gz)
|
|
log "Identified cpmove backup"
|
|
extraction_command="tar -xzf"
|
|
EXTRACTED_SIZE=$(($ARCHIVE_SIZE * 2))
|
|
;;
|
|
backup-*.tar.gz)
|
|
log "Identified full or partial cPanel backup"
|
|
extraction_command="tar -xzf"
|
|
EXTRACTED_SIZE=$(($ARCHIVE_SIZE * 2))
|
|
;;
|
|
*.tar.gz)
|
|
log "Identified gzipped tar backup"
|
|
extraction_command="tar -xzf"
|
|
EXTRACTED_SIZE=$(($ARCHIVE_SIZE * 2))
|
|
;;
|
|
*.tgz)
|
|
log "Identified tgz backup"
|
|
extraction_command="tar -xzf"
|
|
EXTRACTED_SIZE=$(($ARCHIVE_SIZE * 3))
|
|
;;
|
|
*.tar)
|
|
log "Identified tar backup"
|
|
extraction_command="tar -xf"
|
|
EXTRACTED_SIZE=$(($ARCHIVE_SIZE * 3))
|
|
;;
|
|
*.zip)
|
|
log "Identified zip backup"
|
|
extraction_command="unzip"
|
|
EXTRACTED_SIZE=$(($ARCHIVE_SIZE * 3))
|
|
;;
|
|
*)
|
|
log "FATAL ERROR: Unrecognized backup format: $backup_filename"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# CHECK DISK USAGE
|
|
check_if_disk_available(){
|
|
TMP_DIR="/tmp"
|
|
HOME_DIR="/home"
|
|
|
|
# Get available space in /tmp and home directories in bytes
|
|
AVAILABLE_TMP=$(df --output=avail "$TMP_DIR" | tail -n 1)
|
|
AVAILABLE_HOME=$(df --output=avail "$HOME_DIR" | tail -n 1)
|
|
|
|
AVAILABLE_TMP=$(($AVAILABLE_TMP * 1024))
|
|
AVAILABLE_HOME=$(($AVAILABLE_HOME * 1024))
|
|
|
|
# Check if there's enough space
|
|
if [[ $AVAILABLE_TMP -ge $EXTRACTED_SIZE && $AVAILABLE_HOME -ge $EXTRACTED_SIZE ]]; then
|
|
log "There is enough disk space to extract the archive and copy it to the home directory."
|
|
else
|
|
log "FATAL ERROR: Not enough disk space."
|
|
if [[ $AVAILABLE_TMP -lt $EXTRACTED_SIZE ]]; then
|
|
log "Insufficient space in the '/tmp' partition."
|
|
log "Available: $AVAILABLE_TMP - Needed: $EXTRACTED_SIZE"
|
|
fi
|
|
if [[ $AVAILABLE_HOME -lt $EXTRACTED_SIZE ]]; then
|
|
log "Insufficient space in the '/home' directory."
|
|
log "Available: $AVAILABLE_HOME - Needed: $EXTRACTED_SIZE"
|
|
fi
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# EXTRACT
|
|
extract_cpanel_backup() {
|
|
backup_location="$1"
|
|
backup_dir="$2"
|
|
log "Extracting backup from $backup_location to $backup_dir"
|
|
mkdir -p "$backup_dir"
|
|
|
|
# Extract the backup
|
|
if [ "$extraction_command" = "unzip" ]; then
|
|
$extraction_command "$backup_location" -d "$backup_dir"
|
|
else
|
|
$extraction_command "$backup_location" -C "$backup_dir"
|
|
fi
|
|
if [ $? -eq 0 ]; then
|
|
log "Backup extracted successfully."
|
|
else
|
|
log "FATAL ERROR: Backup extraction failed."
|
|
cleanup
|
|
exit 1
|
|
fi
|
|
|
|
# Handle nested archives (common in some cPanel backups)
|
|
for nested_archive in "$backup_dir"/*.tar.gz "$backup_dir"/*.tgz; do
|
|
if [ -f "$nested_archive" ]; then
|
|
log "Found nested archive: $nested_archive"
|
|
tar -xzf "$nested_archive" -C "$backup_dir"
|
|
rm "$nested_archive" # Remove the nested archive after extraction
|
|
fi
|
|
done
|
|
}
|
|
|
|
# LOCATE FILES IN EXTRACTED BACKUP
|
|
locate_backup_directories() {
|
|
log "Locating important files in the extracted backup"
|
|
|
|
# Try to locate the key directories
|
|
homedir=$(find "$backup_dir" -type d -name "homedir" | head -n 1)
|
|
if [ -z "$homedir" ]; then
|
|
homedir=$(find "$backup_dir" -type d -name "public_html" -printf '%h\n' | head -n 1)
|
|
fi
|
|
if [ -z "$homedir" ]; then
|
|
log "FATAL ERROR: Unable to locate home directory in the backup"
|
|
exit 1
|
|
fi
|
|
|
|
mysqldir=$(find "$backup_dir" -type d -name "mysql" | head -n 1)
|
|
if [ -z "$mysqldir" ]; then
|
|
log "WARNING: Unable to locate MySQL directory in the backup"
|
|
fi
|
|
|
|
mysql_conf=$(find "$backup_dir" -type f -name "mysql.sql" | head -n 1)
|
|
if [ -z "$mysql_conf" ]; then
|
|
log "WARNING: Unable to locate MySQL grants file in the backup"
|
|
fi
|
|
|
|
cp_file=$(find "$backup_dir" -type f -path "*/cp/*" -name "$cpanel_username" | head -n 1)
|
|
if [ -z "$cp_file" ]; then
|
|
log "FATAL ERROR: Unable to locate cp/$cpanel_username file in the backup"
|
|
exit 1
|
|
fi
|
|
|
|
log "Backup directories located successfully"
|
|
log "Home directory: $homedir"
|
|
log "MySQL directory: $mysqldir"
|
|
log "MySQL grants: $mysql_conf"
|
|
log "cPanel configuration: $cp_file"
|
|
}
|
|
|
|
# CPANEL BACKUP METADATA
|
|
parse_cpanel_metadata() {
|
|
log "Starting to parse cPanel metadata..."
|
|
|
|
cp_file="${real_backup_files_path}/cp/${cpanel_username}"
|
|
debug_log "Attempting to parse metadata from file: $cp_file"
|
|
|
|
if [ ! -f "$cp_file" ]; then
|
|
log "WARNING: cp file $cp_file not found. Using default values."
|
|
main_domain=""
|
|
cpanel_email=""
|
|
php_version="inherit"
|
|
else
|
|
# Function to get value from cp file with default
|
|
get_cp_value() {
|
|
local key="$1"
|
|
local default="$2"
|
|
local value
|
|
value=$(grep "^$key=" "$cp_file" | cut -d'=' -f2-)
|
|
if [ -z "$value" ]; then
|
|
echo "$default"
|
|
else
|
|
echo "$value"
|
|
fi
|
|
}
|
|
|
|
main_domain=$(get_cp_value "DNS" "")
|
|
cpanel_email=$(get_cp_value "CONTACTEMAIL" "")
|
|
[ -z "$cpanel_email" ] && cpanel_email=$(get_cp_value "CONTACTEMAIL2" "")
|
|
[ -z "$cpanel_email" ] && cpanel_email=$(get_cp_value "EMAIL" "")
|
|
|
|
# Check for Cloudlinux PHP Selector
|
|
cfg_file="${real_backup_files_path}/homedir/.cl.selector/defaults.cfg"
|
|
if [ -f "$cfg_file" ]; then
|
|
php_version=$(grep '^php\s*=' "$cfg_file" | awk -F '=' '{print $2}' | tr -d '[:space:]')
|
|
[ -z "$php_version" ] && php_version="inherit"
|
|
else
|
|
php_version="inherit"
|
|
fi
|
|
|
|
# Additional metadata
|
|
ip_address=$(get_cp_value "IP" "")
|
|
plan=$(get_cp_value "PLAN" "default")
|
|
max_addon=$(get_cp_value "MAXADDON" "0")
|
|
max_ftp=$(get_cp_value "MAXFTP" "unlimited")
|
|
max_sql=$(get_cp_value "MAXSQL" "unlimited")
|
|
max_pop=$(get_cp_value "MAXPOP" "unlimited")
|
|
max_sub=$(get_cp_value "MAXSUB" "unlimited")
|
|
|
|
log "Additional metadata parsed:"
|
|
log "IP Address: $ip_address"
|
|
log "Plan: $plan"
|
|
log "Max Addon Domains: $max_addon"
|
|
log "Max FTP Accounts: $max_ftp"
|
|
log "Max SQL Databases: $max_sql"
|
|
log "Max Email Accounts: $max_pop"
|
|
log "Max Subdomains: $max_sub"
|
|
fi
|
|
|
|
# Ensure we have at least an empty string for each variable
|
|
main_domain="${main_domain:-}"
|
|
cpanel_email="${cpanel_email:-}"
|
|
php_version="${php_version:-inherit}"
|
|
|
|
log "cPanel metadata parsed:"
|
|
log "Main Domain: ${main_domain:-Not found}"
|
|
log "Email: ${cpanel_email:-Not found}"
|
|
log "PHP Version: $php_version"
|
|
log "Finished parsing cPanel metadata."
|
|
}
|
|
|
|
# CHECK BEFORE EXPORT
|
|
check_if_user_exists(){
|
|
backup_filename=$(basename "$backup_location")
|
|
cpanel_username="${backup_filename##*_}"
|
|
cpanel_username="${cpanel_username%%.*}"
|
|
log "Username: $cpanel_username"
|
|
|
|
local existing_user=""
|
|
if opencli user-list --json > /dev/null 2>&1; then
|
|
existing_user=$(opencli user-list --json | jq -r ".[] | select(.username == \"$cpanel_username\") | .id")
|
|
fi
|
|
if [ -z "$existing_user" ]; then
|
|
log "Username $cpanel_username is available, starting import.."
|
|
else
|
|
log "FATAL ERROR: $cpanel_username already exists."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# CREATE NEW USER
|
|
create_new_user() {
|
|
local username="$1"
|
|
local email="$3"
|
|
local plan_name="$4"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would create user $username with email $email and plan $plan_name"
|
|
return
|
|
fi
|
|
|
|
create_user_command=$(opencli user-add "$cpanel_username" generate "$email" "$plan_name" 2>&1)
|
|
while IFS= read -r line; do
|
|
log "$line"
|
|
done <<< "$create_user_command"
|
|
|
|
if echo "$create_user_command" | grep -q "Successfully added user"; then
|
|
:
|
|
else
|
|
log "FATAL ERROR: User addition failed. Response did not contain the expected success message."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# PHP VERSION
|
|
restore_php_version() {
|
|
local php_version="$1"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would check/install PHP version $php_version for user $cpanel_username"
|
|
return
|
|
fi
|
|
|
|
# Check if php_version is "inherit"
|
|
if [ "$php_version" == "inherit" ]; then
|
|
log "PHP version is set to inherit. No changes will be made."
|
|
else
|
|
log "Checking if current PHP version installed matches the version from backup"
|
|
local current_version=$(opencli php-default_php_version "$cpanel_username" | sed 's/Default PHP version for user.*: //')
|
|
if [ "$current_version" != "$php_version" ]; then
|
|
local installed_versions=$(opencli php-enabled_php_versions "$cpanel_username")
|
|
if ! echo "$installed_versions" | grep -q "$php_version"; then
|
|
log "Default PHP version $php_version from backup is not present in the container, installing.."
|
|
output=$(opencli php-install_php_version "$cpanel_username" "$php_version" 2>&1)
|
|
while IFS= read -r line; do
|
|
log "$line"
|
|
done <<< "$output"
|
|
|
|
#SET AS DEFAULT PHP VERSION
|
|
log "Setting newly installed PHP $php_version as the default version for all new domains."
|
|
output=$(opencli php-default_php_version "$cpanel_username" --update "$php_version" 2>&1)
|
|
while IFS= read -r line; do
|
|
log "$line"
|
|
done <<< "$output"
|
|
fi
|
|
else
|
|
log "Default PHP version in backup file ($php_version) matches the installed PHP version: ($current_version) "
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# PHPMYADMIN
|
|
grant_phpmyadmin_access() {
|
|
local username="$1"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would grant phpMyAdmin access to all databases for user $username"
|
|
return
|
|
fi
|
|
|
|
log "Granting phpMyAdmin access to all databases for user $username"
|
|
# https://github.com/stefanpejcic/OpenPanel/blob/148b5e482f7bde4850868ba5cf85717538770882/docker/apache/phpmyadmin/pma.php#L13C44-L13C54
|
|
phpmyadmin_user="phpmyadmin"
|
|
sql_command="GRANT ALL ON *.* TO 'phpmyadmin'@'localhost'; FLUSH PRIVILEGES;"
|
|
grant_commands=$(docker exec $username mysql -N -e "$sql_command")
|
|
|
|
log "Access granted to phpMyAdmin user for all databases of $username"
|
|
|
|
}
|
|
|
|
# MYSQL
|
|
restore_mysql() {
|
|
local mysql_dir="$1"
|
|
local sandbox_warning_logged=false
|
|
|
|
log "Restoring MySQL databases for user $cpanel_username"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would restore MySQL databases for user $cpanel_username"
|
|
return
|
|
fi
|
|
|
|
#https://jira.mariadb.org/browse/MDEV-34183
|
|
apply_sandbox_workaround() {
|
|
local db_file="$1"
|
|
text_to_check='enable the sandbox mode'
|
|
local first_line
|
|
|
|
first_line=$(head -n 1 ${real_backup_files_path}/mysql/$db_file)
|
|
if echo "$first_line" | grep -q "$text_to_check"; then
|
|
if [ "$sandbox_warning_logged" = false ]; then
|
|
log "WARNING: Database dumps were created on a MariaDB server with '--sandbox' mode. Applying workaround for backwards compatibility to MySQL (BUG: https://jira.mariadb.org/browse/MDEV-34183)"
|
|
sandbox_warning_logged=true
|
|
fi
|
|
# Remove the first line and save the changes to the same file
|
|
tail -n +2 "${real_backup_files_path}/mysql/$db_file" > "${real_backup_files_path}/mysql/${db_file}.workaround" && mv "${real_backup_files_path}/mysql/${db_file}.workaround" "${real_backup_files_path}/mysql/$db_file"
|
|
fi
|
|
}
|
|
|
|
if [ -d "$mysql_dir" ]; then
|
|
# STEP 1. get old server ip and replace it in the mysql.sql file that has import permissions
|
|
old_ip=$(grep -oP 'IP=\K[0-9.]+' ${real_backup_files_path}/cp/$cpanel_username)
|
|
log "Replacing old server IP: $old_ip with '%' in database grants"
|
|
sed -i "s/$old_ip/%/g" $mysql_conf
|
|
|
|
old_hostname=$(cat ${real_backup_files_path}/meta/hostname)
|
|
log "Removing old hostname $old_hostname from database grants"
|
|
sed -i "/$old_hostname/d" "$mysql_conf"
|
|
|
|
|
|
# STEP 2. start mysql for user
|
|
log "Initializing MySQL service for user"
|
|
docker exec $cpanel_username bash -c "service mysql start >/dev/null 2>&1"
|
|
docker exec "$cpanel_username" sed -i 's/CRON_STATUS="off"/CRON_STATUS="on"/' /etc/entrypoint.sh
|
|
|
|
# STEP 3. create and import databases
|
|
total_databases=$(ls "$mysql_dir"/*.create | wc -l)
|
|
log "Starting import for $total_databases MySQL databases"
|
|
if [ "$total_databases" -gt 0 ]; then
|
|
current_db=1
|
|
for db_file in "$mysql_dir"/*.create; do
|
|
local db_name=$(basename "$db_file" .create)
|
|
|
|
log "Creating database: $db_name (${current_db}/${total_databases})"
|
|
apply_sandbox_workaround "$db_name.create" # Apply the workaround if it's needed
|
|
docker cp ${real_backup_files_path}/mysql/$db_name.create $cpanel_username:/tmp/${db_name}.create >/dev/null 2>&1
|
|
docker exec $cpanel_username bash -c "mysql < /tmp/${db_name}.create && rm /tmp/${db_name}.create"
|
|
|
|
log "Importing tables for database: $db_name"
|
|
apply_sandbox_workaround "$db_name.sql" # Apply the workaround if it's needed
|
|
docker cp ${real_backup_files_path}/mysql/$db_name.sql $cpanel_username:/tmp/$db_name.sql >/dev/null 2>&1
|
|
docker exec $cpanel_username bash -c "mysql ${db_name} < /tmp/${db_name}.sql && rm /tmp/${db_name}.sql"
|
|
current_db=$((current_db + 1))
|
|
done
|
|
log "Finished processing $current_db databases"
|
|
else
|
|
log "WARNING: No MySQL databases found"
|
|
fi
|
|
# STEP 4. import grants and flush privileges
|
|
log "Importing database grants"
|
|
python3 $script_dir/mysql/json_2_sql.py ${real_backup_files_path}/mysql.sql ${real_backup_files_path}/mysql.TEMPORARY.sql >/dev/null 2>&1
|
|
|
|
docker cp ${real_backup_files_path}/mysql.TEMPORARY.sql $cpanel_username:/tmp/mysql.TEMPORARY.sql >/dev/null 2>&1
|
|
docker exec $cpanel_username bash -c "mysql < /tmp/mysql.TEMPORARY.sql && mysql -e 'FLUSH PRIVILEGES;' && rm /tmp/mysql.TEMPORARY.sql"
|
|
|
|
# STEP 5. Grant phpMyAdmin access
|
|
grant_phpmyadmin_access "$cpanel_username"
|
|
|
|
else
|
|
log "No MySQL databases found to restore"
|
|
fi
|
|
}
|
|
|
|
# SSL CACHE
|
|
refresh_ssl_file() {
|
|
local username="$1"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would refresh SSL file for user $username"
|
|
return
|
|
fi
|
|
|
|
log "Creating a list of SSL certificates for user interface"
|
|
opencli ssl-user "$cpanel_username"
|
|
|
|
}
|
|
|
|
# SSL CERTIFICATES
|
|
restore_ssl() {
|
|
local username="$1"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would restore SSL certificates for user $username"
|
|
return
|
|
fi
|
|
|
|
log "Restoring SSL certificates for user $username"
|
|
if [ -d "$real_backup_files_path/ssl" ]; then
|
|
for cert_file in "$real_backup_files_path/ssl"/*.crt; do
|
|
local domain=$(basename "$cert_file" .crt)
|
|
local key_file="$real_backup_files_path/ssl/$domain.key"
|
|
if [ -f "$key_file" ]; then
|
|
log "Installing SSL certificate for domain: $domain"
|
|
opencli ssl install --domain "$domain" --cert "$cert_file" --key "$key_file"
|
|
else
|
|
log "SSL key file not found for domain: $domain"
|
|
fi
|
|
done
|
|
|
|
# Refresh the SSL file after restoring certificates
|
|
refresh_ssl_file "$username"
|
|
else
|
|
log "No SSL certificates found to restore"
|
|
fi
|
|
|
|
}
|
|
|
|
# SSH KEYS
|
|
restore_ssh() {
|
|
local username="$1"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would restore SSH access for user $username"
|
|
return
|
|
fi
|
|
|
|
log "Restoring SSH access for user $username"
|
|
local shell_access=$(grep -oP 'shell: \K\S+' "$real_backup_files_path/userdata/main")
|
|
if [ "$shell_access" == "/bin/bash" ]; then
|
|
opencli user-ssh enable "$username"
|
|
if [ -f "$real_backup_files_path/.ssh/id_rsa.pub" ]; then
|
|
mkdir -p "/home/$username/.ssh"
|
|
cp "$real_backup_files_path/.ssh/id_rsa.pub" "/home/$username/.ssh/authorized_keys"
|
|
chown -R "$username:$username" "/home/$username/.ssh"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# DNS ZONES
|
|
restore_dns_zones() {
|
|
log "Restoring DNS zones for user $cpanel_username"
|
|
|
|
|
|
|
|
|
|
#domain_file="$real_backup_files_path/userdata/$domain"
|
|
#domain=$(basename "$domain_file")
|
|
|
|
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would restore DNS zones for user $cpanel_username"
|
|
return
|
|
fi
|
|
|
|
if [ -d "$real_backup_files_path/dnszones" ]; then
|
|
for zone_file in "$real_backup_files_path/dnszones"/*; do
|
|
local zone_name=$(basename "${zone_file%.db}")
|
|
|
|
# Check if the destination zone file exists, if not, it was probably a subdomain that had no dns zone and
|
|
if [ ! -f "/etc/bind/zones/${zone_name}.zone" ]; then
|
|
log "DNS zone file /etc/bind/zones/${zone_name}.zone does not exist. Skipping import for $zone_name."
|
|
continue
|
|
else
|
|
log "Importing DNS zone: $zone_name"
|
|
fi
|
|
|
|
old_ip=$(grep -oP 'IP=\K[0-9.]+' ${real_backup_files_path}/cp/$cpanel_username)
|
|
if [ -z "$old_ip" ]; then
|
|
log "WARNING: old server ip address not detected in file ${real_backup_files_path}/cp/$cpanel_username - records will not be automatically updated to new ip address."
|
|
else
|
|
log "Replacing old server IP: $old_ip with new IP: $new_ip in DNS zone file for domain: $zone_name"
|
|
sed -i "s/$old_ip/$new_ip/g" $zone_file
|
|
fi
|
|
|
|
# Temporary files to store intermediate results
|
|
temp_file_of_original_zone=$(mktemp)
|
|
temp_file_of_created_zone=$(mktemp)
|
|
|
|
# Remove all lines after the last line that starts with '@'
|
|
log "Editing original zone for domain $zone_name to temporary file: $temp_file_of_original_zone"
|
|
awk '/^@/ { found=1; last_line=NR } { if (found && NR > last_line) exit } { print }' "$zone_file" > "$temp_file_of_original_zone"
|
|
|
|
# Remove all lines from the beginning until the line that has 'NS' and including that line
|
|
log "Editing created zone for domain $zone_name to temporary file: $temp_file_of_created_zone"
|
|
awk '/NS/ { found=1; next } found { print }' "/etc/bind/zones/${zone_name}.zone" > "$temp_file_of_created_zone"
|
|
|
|
# Append the processed second file to the first
|
|
log "Merging the DNS zone records from $temp_file_of_created_zone with $temp_file_of_original_zone"
|
|
cat "$temp_file_of_created_zone" >> "$temp_file_of_original_zone"
|
|
|
|
# Move the merged content to the final file
|
|
log "Replacing the created zone /etc/bind/zones/${zone_name}.zone with updated records."
|
|
mv "$temp_file_of_original_zone" "/etc/bind/zones/${zone_name}.zone"
|
|
|
|
# Clean up
|
|
rm "$temp_file_of_created_zone"
|
|
|
|
log "DNS zone file for $zone_name has been imported."
|
|
done
|
|
else
|
|
log "No DNS zones found to restore"
|
|
fi
|
|
}
|
|
|
|
# HOME DIR
|
|
restore_files() {
|
|
du_needed_for_home=$(du -sh "$real_backup_files_path/homedir" | cut -f1)
|
|
log "Restoring files ($du_needed_for_home) to /home/$cpanel_username/"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would restore files to /home/$cpanel_username/"
|
|
return
|
|
fi
|
|
|
|
mv $real_backup_files_path/homedir /home/$cpanel_username
|
|
|
|
: '
|
|
rsync -Prltvc --info=progress2 "$real_backup_files_path/homedir/" "/home/$cpanel_username/" 2>&1 | while IFS= read -r line; do
|
|
log "$line"
|
|
done
|
|
|
|
log "Finished transferring files, comparing to source.."
|
|
original_size=$(du -sb "$real_backup_files_path/homedir" | cut -f1)
|
|
copied_size=$(du -sb "/home/$cpanel_username/" | cut -f1)
|
|
|
|
if [[ "$original_size" -eq "$copied_size" ]]; then
|
|
log "The original and target directories have the same size."
|
|
else
|
|
log "WARNING: The original and target directories differ in size after restore."
|
|
log "Original size: $original_size bytes"
|
|
log "Target size: $copied_size bytes"
|
|
fi
|
|
'
|
|
|
|
# Move all files from public_html to main domain dir
|
|
log "Moving main domain files from public_html to $main_domain directory."
|
|
mv /home/$cpanel_username/public_html /home/$cpanel_username/$main_domain
|
|
rm /home/$cpanel_username/www #since www is symlink to public_html
|
|
|
|
#shopt -s dotglob
|
|
#mv "/home/$cpanel_username/public_html"/* "/home/$cpanel_username/$main_domain"/
|
|
#shopt -u dotglob
|
|
}
|
|
|
|
# PERMISSIONS
|
|
fix_perms(){
|
|
log "Changing permissions for all files and folders in user home directory /home/$cpanel_username/"
|
|
docker exec $cpanel_username bash -c "chown -R 1000:33 /home/$cpanel_username"
|
|
}
|
|
|
|
|
|
# WORDPRESS SITES
|
|
restore_wordpress() {
|
|
local real_backup_files_path="$1"
|
|
local username="$2"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would restore WordPress sites for user $username"
|
|
return
|
|
fi
|
|
|
|
log "Restoring WordPress sites for user $username"
|
|
if [ -d "$real_backup_files_path/wptoolkit" ]; then
|
|
for wp_file in "$real_backup_files_path/wptoolkit"/*.json; do
|
|
log "Importing WordPress site from: $wp_file"
|
|
opencli wp-import "$username" "$wp_file"
|
|
done
|
|
else
|
|
log "No WordPress data found to restore"
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
|
|
# DOMAINS
|
|
restore_domains() {
|
|
if [ -f "$real_backup_files_path/userdata/main" ]; then
|
|
file_path="$real_backup_files_path/userdata/main"
|
|
# Initialize variables
|
|
main_domain=""
|
|
parked_domains=""
|
|
sub_domains=""
|
|
addon_domains=""
|
|
|
|
# Read the file line by line
|
|
while IFS= read -r line; do
|
|
if [[ "$line" =~ ^main_domain: ]]; then
|
|
main_domain=$(echo "$line" | awk '{print $2}')
|
|
elif [[ "$line" =~ ^parked_domains: ]]; then
|
|
parked_domains=$(echo "$line" | awk '{print $2}' | tr -d '[]')
|
|
elif [[ "$line" =~ ^sub_domains: ]]; then
|
|
sub_domains_section=true
|
|
continue
|
|
elif [[ "$line" =~ ^addon_domains: ]]; then
|
|
addon_domains_section=true
|
|
continue
|
|
fi
|
|
|
|
if [[ "$sub_domains_section" == true ]]; then
|
|
if [[ "$line" =~ ^[[:space:]]+- ]]; then
|
|
sub_domains+=$(echo "$line" | awk '{print $2}')$'\n'
|
|
else
|
|
sub_domains_section=false
|
|
fi
|
|
fi
|
|
|
|
if [[ "$addon_domains_section" == true ]]; then
|
|
if [[ "$line" =~ ^[[:space:]]*[^:]+:[[:space:]]*[^[:space:]]+$ ]]; then
|
|
domain=$(echo "$line" | awk -F: '{print $1}' | tr -d '[:space:]')
|
|
# Avoid adding invalid entries and trailing colons
|
|
if [[ -n "$domain" && "$domain" != "main_domain" && "$domain" != "parked_domains" ]]; then
|
|
addon_domains+="$domain"$'\n'
|
|
fi
|
|
else
|
|
addon_domains_section=false
|
|
fi
|
|
fi
|
|
done < "$file_path"
|
|
|
|
# Parse parked_domains
|
|
if [[ -z "$parked_domains" ]]; then
|
|
parked_domains_array=()
|
|
else
|
|
IFS=',' read -r -a parked_domains_array <<< "$parked_domains"
|
|
fi
|
|
|
|
sub_domains_array=()
|
|
addon_domains_array=()
|
|
|
|
# Parse sub_domains
|
|
while IFS= read -r domain; do
|
|
if [[ -n "$domain" ]]; then
|
|
sub_domains_array+=("$domain")
|
|
fi
|
|
done <<< "$sub_domains"
|
|
|
|
# Parse addon_domains
|
|
while IFS= read -r domain; do
|
|
if [[ -n "$domain" ]]; then
|
|
addon_domains_array+=("$domain")
|
|
fi
|
|
done <<< "$addon_domains"
|
|
|
|
# Filter out subdomains that are essentially addon_domain.$main_domain
|
|
filtered_sub_domains=()
|
|
for sub_domain in "${sub_domains_array[@]}"; do
|
|
trimmed_sub_domain=$(echo "$sub_domain" | xargs)
|
|
is_addon=false
|
|
for addon in "${addon_domains_array[@]}"; do
|
|
if [[ "$trimmed_sub_domain" == "$addon.$main_domain" ]]; then
|
|
is_addon=true
|
|
break
|
|
fi
|
|
done
|
|
if [ "$is_addon" = false ]; then
|
|
filtered_sub_domains+=("$trimmed_sub_domain")
|
|
fi
|
|
done
|
|
|
|
main_domain_count=1
|
|
|
|
addon_domains_count=${#addon_domains_array[@]}
|
|
if [ "${#addon_domains_array[@]}" -eq 1 ] && [ -z "${addon_domains_array[0]}" ]; then
|
|
addon_domains_count=0
|
|
log "No addon domains detected."
|
|
else
|
|
log "Addon domains ($addon_domains_count): ${addon_domains_array[@]}"
|
|
fi
|
|
|
|
parked_domains_count=${#parked_domains_array[@]}
|
|
if [ "${#parked_domains_array[@]}" -eq 1 ] && [ -z "${parked_domains_array[0]}" ]; then
|
|
parked_domains_count=0
|
|
log "No parked domains detected."
|
|
else
|
|
log "Parked domains ($parked_domains_count): ${parked_domains_array[@]}"
|
|
fi
|
|
|
|
filtered_sub_domains_count=${#filtered_sub_domains[@]}
|
|
if [ "${#filtered_sub_domains[@]}" -eq 1 ] && [ -z "${filtered_sub_domains[0]}" ]; then
|
|
filtered_sub_domains_count=0
|
|
log "No subdomains detected."
|
|
else
|
|
log "Subdomains ($filtered_sub_domains_count): ${filtered_sub_domains[@]}"
|
|
fi
|
|
|
|
|
|
domains_total_count=$((main_domain_count + addon_domains_count + parked_domains_count + filtered_sub_domains_count))
|
|
|
|
log "Detected a total of $domains_total_count domains for user."
|
|
|
|
current_domain_count=0
|
|
|
|
create_domain(){
|
|
domain="$1"
|
|
type="$2"
|
|
|
|
current_domain_count=$((current_domain_count + 1))
|
|
log "Restoring $type $domain (${current_domain_count}/${domains_total_count})"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would restore $type $domain"
|
|
elif opencli domains-whoowns "$domain" | grep -q "not found in the database."; then
|
|
output=$(opencli domains-add "$domain" "$cpanel_username" 2>&1)
|
|
while IFS= read -r line; do
|
|
log "$line"
|
|
done <<< "$output"
|
|
else
|
|
log "WARNING: $type $domain already exists and will not be added to this user."
|
|
fi
|
|
}
|
|
|
|
# Process the domains
|
|
log "Processing main (primary) domain.."
|
|
create_domain "$main_domain" "main domain"
|
|
|
|
if [ "$parked_domains_count" -eq 0 ]; then
|
|
log "No parked (alias) domains detected."
|
|
else
|
|
log "Processing parked (alias) domains.."
|
|
for parked in "${parked_domains_array[@]}"; do
|
|
create_domain "$parked" "alias domain"
|
|
done
|
|
fi
|
|
|
|
if [ "$addon_domains_count" -eq 0 ]; then
|
|
log "No addon domains detected."
|
|
else
|
|
log "Processing addon domains.."
|
|
for addon in "${addon_domains_array[@]}"; do
|
|
create_domain "$addon" "addon domain"
|
|
done
|
|
fi
|
|
|
|
if [ "$filtered_sub_domains_count" -eq 0 ]; then
|
|
log "No subdomains detected."
|
|
else
|
|
log "Processing sub-domains.."
|
|
for filtered_sub in "${filtered_sub_domains[@]}"; do
|
|
create_domain "$filtered_sub" "subdomain"
|
|
#TODO: create record in dns zone instead of separate domain if only dns zone and no folder!
|
|
done
|
|
fi
|
|
|
|
log "Finished importing $domains_total_count domains"
|
|
|
|
else
|
|
log "FATAL ERROR: domains file userdata/main is missing in backup file."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# CRONJOB
|
|
restore_cron() {
|
|
log "Restoring cron jobs for user $cpanel_username"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would restore cron jobs for user $cpanel_username"
|
|
return
|
|
fi
|
|
|
|
if [ -f "$real_backup_files_path/cron/$cpanel_username" ]; then
|
|
# exclude shell and email variables from file!
|
|
sed -i '1,2d' "$real_backup_files_path/cron/$cpanel_username"
|
|
|
|
output=$(docker cp $real_backup_files_path/cron/$cpanel_username $cpanel_username:/var/spool/cron/crontabs/$cpanel_username 2>&1)
|
|
while IFS= read -r line; do
|
|
log "$line"
|
|
done <<< "$output"
|
|
|
|
output=$(docker exec $cpanel_username bash -c "crontab -u $cpanel_username /var/spool/cron/crontabs/$cpanel_username" 2>&1)
|
|
while IFS= read -r line; do
|
|
log "$line"
|
|
done <<< "$output"
|
|
|
|
output=$(docker exec $cpanel_username bash -c "service cron restart" 2>&1)
|
|
while IFS= read -r line; do
|
|
log "$line"
|
|
done <<< "$output"
|
|
|
|
docker exec "$cpanel_username" sed -i 's/CRON_STATUS="off"/CRON_STATUS="on"/' /etc/entrypoint.sh >/dev/null 2>&1
|
|
else
|
|
log "No cron jobs found to restore"
|
|
fi
|
|
}
|
|
|
|
# Main execution
|
|
main() {
|
|
echo -e "
|
|
------------------ STARTING CPANEL ACCOUNT IMPORT ------------------
|
|
--------------------------------------------------------------------
|
|
|
|
Currently supported features:
|
|
- FILES AND FOLDERS
|
|
- DOMAINS: MAIN, ADDONS, ALIASES, SUBDOMAINS
|
|
- DNS ZONES
|
|
- MYSQL DATABASES, USERS AND THEIR GRANTS
|
|
- PHP VERSIONS FROM CLOUDLINUX SELECTOR
|
|
- SSH KEYS
|
|
- CRONJOBS
|
|
- WP SITES FROM WPTOOLKIT OR SOFTACULOUS
|
|
|
|
emails, nodejs/python apps and postgres are not yet supported!
|
|
|
|
--------------------------------------------------------------------
|
|
if you experience any errors with this script, please report to
|
|
https://github.com/stefanpejcic/cPanel-to-OpenPanel/issues
|
|
--------------------------------------------------------------------
|
|
"
|
|
|
|
log "Log file: $log_file"
|
|
log "PID: $pid"
|
|
|
|
# PRE-RUN CHECKS
|
|
check_if_valid_cp_backup "$backup_location"
|
|
check_if_disk_available
|
|
check_if_user_exists
|
|
validate_plan_exists
|
|
install_dependencies
|
|
get_server_ipv4 #used in mysql grants
|
|
|
|
# unique
|
|
backup_dir=$(mktemp -d /tmp/cpanel_import_XXXXXX)
|
|
log "Created temporary directory: $backup_dir"
|
|
|
|
# extract
|
|
extract_cpanel_backup "$backup_location" "$backup_dir"
|
|
real_backup_files_path=$(find "$backup_dir" -type f -name "version" | head -n 1 | xargs dirname)
|
|
log "Extracted backup folder: $real_backup_files_path"
|
|
|
|
# locate important directories
|
|
locate_backup_directories
|
|
parse_cpanel_metadata
|
|
|
|
|
|
|
|
# its faster to restore home dir, then create user
|
|
restore_files
|
|
create_new_user "$cpanel_username" "random" "$cpanel_email" "$plan_name"
|
|
|
|
fix_perms
|
|
restore_php_version "$php_version" # php v needs to run before domains
|
|
restore_domains
|
|
restore_dns_zones
|
|
restore_mysql "$mysqldir"
|
|
restore_cron
|
|
restore_ssl "$cpanel_username"
|
|
restore_ssh "$cpanel_username"
|
|
restore_wordpress "$real_backup_files_path" "$cpanel_username"
|
|
|
|
#todo:
|
|
# ftp accounts from proftpdpasswd file
|
|
|
|
# Cleanup
|
|
cleanup
|
|
|
|
end_time=$(date +%s)
|
|
elapsed=$(( end_time - start_time ))
|
|
hours=$(( elapsed / 3600 ))
|
|
minutes=$(( (elapsed % 3600) / 60 ))
|
|
seconds=$(( elapsed % 60 ))
|
|
|
|
log "Elapsed time: ${hours}h ${minutes}m ${seconds}s"
|
|
|
|
log "SUCCESS: Import for user $cpanel_username completed successfully."
|
|
|
|
|
|
# run after install if posthook provided
|
|
if [ -n "$post_hook" ]; then
|
|
if [ -x "$post_hook" ]; then
|
|
log "Executing post-hool script.."
|
|
"$post_hook" "$cpanel_username"
|
|
else
|
|
log "WARNING: Post-hook file '$post_hook' is not executable or not found."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
# MAIN FUNCTION
|
|
define_data_and_log "$@"
|