diff --git a/platform/restore-myapps.sh b/platform/restore-myapps.sh index 367adfe..eed0e00 100755 --- a/platform/restore-myapps.sh +++ b/platform/restore-myapps.sh @@ -1,6 +1,19 @@ #!/bin/bash # ============================================= -# restore-myapps.sh โ€” Complete Restore Script +# restore-myapps.sh โ€” Smart Restore Script +# +# Can run: +# - Locally on the server/vm: +# ./restore-myapps.sh +# +# - Remotely (from VM targeting the main server, or vice versa): +# ./restore-myapps.sh --remote [--key /path/to/key | --password] +# +# Features: +# - Skips containers that are already healthy/running +# - Applies all known post-restore fixes per app +# - Detects target IP automatically or uses provided one +# - Works whether run locally or proxied over SSH # ============================================= set -uo pipefail @@ -12,8 +25,8 @@ REMOTE_MODE=false REMOTE_IP="" REMOTE_USER="root" SSH_KEY="" +SSH_PASSWORD="" USE_PASSWORD=false -SSH_PASS="" while [[ $# -gt 0 ]]; do case "$1" in @@ -38,91 +51,153 @@ while [[ $# -gt 0 ]]; do done # -------------------------------------------------- -# If remote mode +# If remote mode: copy this script + backup to target and run it there # -------------------------------------------------- if [ "$REMOTE_MODE" = true ]; then if [ -z "$REMOTE_IP" ]; then echo "โŒ --remote requires an IP address." + echo " Usage: $0 --remote [USER] [--key /path/key]" exit 1 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REMOTE_DEST="/tmp/restore-session-$(date +%s)" + # Build SSH/SCP options SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=15" if [ -n "$SSH_KEY" ]; then SSH_OPTS="$SSH_OPTS -i $SSH_KEY" - fi - - if [ "$USE_PASSWORD" = true ]; then + SCP_OPTS="$SSH_OPTS" + elif [ "$USE_PASSWORD" = true ]; then if ! command -v sshpass &>/dev/null; then - apt install sshpass -y + echo "โŒ sshpass not installed. Install it with: apt install sshpass" + exit 1 fi read -s -p "SSH password for ${REMOTE_USER}@${REMOTE_IP}: " SSH_PASS echo "" - SSH_CMD="sshpass -p '$SSH_PASS' ssh $SSH_OPTS ${REMOTE_USER}@${REMOTE_IP}" + SSH_CMD="sshpass -p '$SSH_PASS' ssh $SSH_OPTS" SCP_CMD="sshpass -p '$SSH_PASS' scp $SSH_OPTS" - else - SSH_CMD="ssh $SSH_OPTS ${REMOTE_USER}@${REMOTE_IP}" - SCP_CMD="scp $SSH_OPTS" + fi + + SSH_CMD="ssh $SSH_OPTS ${REMOTE_USER}@${REMOTE_IP}" + SCP_CMD="scp $SSH_OPTS" + if [ -n "$SSH_KEY" ]; then + SCP_CMD="scp -i $SSH_KEY $SSH_OPTS" fi echo "=========================================" - echo "๐Ÿ“ก REMOTE RESTORE to ${REMOTE_USER}@${REMOTE_IP}" + echo "๐Ÿ“ก REMOTE RESTORE MODE" + echo " Target: ${REMOTE_USER}@${REMOTE_IP}" + echo " Backup: $SCRIPT_DIR" echo "=========================================" + echo "" + echo "๐Ÿ“ค Copying backup to remote server..." $SSH_CMD "mkdir -p $REMOTE_DEST" $SCP_CMD -r "$SCRIPT_DIR/." "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DEST}/" + + if [ $? -ne 0 ]; then + echo "โŒ Failed to copy backup to remote server." + exit 1 + fi + + echo "โœ… Backup copied." + echo "" + echo "๐Ÿš€ Running restore on remote server..." $SSH_CMD "chmod +x $REMOTE_DEST/restore-myapps.sh && cd $REMOTE_DEST && bash restore-myapps.sh" + + echo "" + echo "๐Ÿงน Cleaning up remote temp files..." $SSH_CMD "rm -rf $REMOTE_DEST" - echo "โœ… Remote restore complete" + echo "=========================================" + echo "โœ… Remote restore complete on $REMOTE_IP" + echo "=========================================" exit 0 fi # =================================================== -# LOCAL RESTORE +# LOCAL RESTORE (runs directly on the target machine) # =================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" +# Detect IP of this machine VM_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v 127.0.0.1 | head -1) echo "=========================================" -echo "๐Ÿ”„ FULL RESTORE โ€” LOCAL MODE" +echo "๐Ÿ”„ Smart Restore โ€” LOCAL MODE" echo " Machine IP: $VM_IP" echo " Backup dir: $SCRIPT_DIR" echo "=========================================" # -------------------------------------------------- -# STEP 1: Restore Volumes +# Helper: check if a container is healthy/running +# Returns 0 (true) if container should be skipped +# -------------------------------------------------- +container_is_healthy() { + local name="$1" + local status + status=$(docker inspect --format='{{.State.Status}}' "$name" 2>/dev/null || echo "missing") + local health + health=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$name" 2>/dev/null || echo "none") + + if [ "$status" = "running" ] && { [ "$health" = "healthy" ] || [ "$health" = "none" ]; }; then + return 0 # healthy โ†’ skip + fi + return 1 # not healthy โ†’ restore +} + +# -------------------------------------------------- +# 1. Restore volumes โ€” skip if container using it is healthy # -------------------------------------------------- echo "" +echo "=========================================" echo "๐Ÿ“ฆ STEP 1 โ€” Restoring Volumes" echo "=========================================" -if [ -d "$SCRIPT_DIR/volumes" ]; then - cd "$SCRIPT_DIR/volumes" - for backup in *.tar.gz; do - [ -f "$backup" ] || continue - volume=$(basename "$backup" .tar.gz) - echo -n " ๐Ÿ“ Restoring $volume ... " - docker volume create "$volume" &>/dev/null || true - docker run --rm \ - -v "${volume}:/target" \ - -v "$(pwd):/backup" \ - alpine \ - sh -c "cd /target && tar xzf /backup/$backup" \ - && echo "โœ…" || echo "โš ๏ธ FAILED" - done - cd "$SCRIPT_DIR" -fi +declare -A VOLUME_OWNERS=( + ["frappe-setup_frappe-sites"]="frappe-erpnext" + ["frappe-setup_mariadb-data"]="frappe-mariadb" + ["nextcloud-setup_nextcloud-data"]="nextcloud-app" + ["nextcloud-setup_nextcloud-db-data"]="nextcloud-postgres" + ["mautic-setup_mautic-data"]="mautic-app" + ["mautic-setup_mautic-db-data"]="mautic-mariadb" + ["n8n-setup_n8n-data"]="n8n-app" + ["n8n-setup_n8n-db-data"]="n8n-db" + ["odoo-clean_db-data"]="odoo-clean-db-1" + ["odoo-clean_odoo-etc"]="odoo-clean-odoo-1" +) + +cd "$SCRIPT_DIR/volumes" +for backup in *.tar.gz; do + [ -f "$backup" ] || continue + volume=$(basename "$backup" .tar.gz) + owner="${VOLUME_OWNERS[$volume]:-}" + + # If the owning container is healthy, skip this volume + if [ -n "$owner" ] && container_is_healthy "$owner"; then + echo " โญ๏ธ $volume โ€” container '$owner' is healthy, skipping" + continue + fi + + echo -n " ๐Ÿ“ Restoring $volume ... " + docker volume create "$volume" &>/dev/null || true + docker run --rm \ + -v "${volume}:/target" \ + -v "$(pwd):/backup" \ + alpine \ + sh -c "cd /target && tar xzf /backup/$backup" \ + && echo "โœ…" || echo "โš ๏ธ FAILED" +done +cd "$SCRIPT_DIR" # -------------------------------------------------- -# STEP 2: Start Containers +# 2. Start containers โ€” skip healthy ones # -------------------------------------------------- echo "" +echo "=========================================" echo "๐Ÿš€ STEP 2 โ€” Starting Containers" echo "=========================================" @@ -134,139 +209,190 @@ declare -A APP_DIRS=( ["n8n"]="n8n-setup" ) -if [ -d "$SCRIPT_DIR/compose-files" ]; then - cd "$SCRIPT_DIR/compose-files" - for app in Frappe Odoo Nextcloud Mautic n8n; do - dir="${APP_DIRS[$app]}" - if [ -d "$dir" ]; then - echo " ๐Ÿš€ Starting $app..." - cd "$dir" - docker-compose up -d 2>&1 | tail -3 - cd .. - fi - done - cd "$SCRIPT_DIR" -fi +declare -A APP_MAIN_CONTAINER=( + ["Frappe"]="frappe-erpnext" + ["Odoo"]="odoo-clean-odoo-1" + ["Nextcloud"]="nextcloud-app" + ["Mautic"]="mautic-app" + ["n8n"]="n8n-app" +) + +cd "$SCRIPT_DIR/compose-files" +for app in Frappe Odoo Nextcloud Mautic n8n; do + dir="${APP_DIRS[$app]}" + main_ctr="${APP_MAIN_CONTAINER[$app]}" + + if container_is_healthy "$main_ctr"; then + echo " โญ๏ธ $app โ€” already running and healthy, skipping" + continue + fi + + if [ -d "$dir" ]; then + echo " ๐Ÿš€ Starting $app..." + cd "$dir" + docker-compose up -d 2>&1 | tail -3 + cd .. + else + echo " โญ๏ธ $app compose dir '$dir' not found, skipping" + fi +done +cd "$SCRIPT_DIR" echo "" echo "โณ Waiting 60s for containers to initialize..." sleep 60 # -------------------------------------------------- -# STEP 3: Post-Restore Fixes +# 3. Post-restore fixes # -------------------------------------------------- echo "" +echo "=========================================" echo "๐Ÿ”ง STEP 3 โ€” Applying Post-Restore Fixes" echo "=========================================" # ---- NEXTCLOUD ---- echo "" echo "๐Ÿ“Œ Nextcloud โ€” Trusted domains..." -if docker ps | grep -q nextcloud-app; then - docker exec nextcloud-app php /var/www/html/occ config:system:delete trusted_domains 1 2>/dev/null || true - docker exec nextcloud-app php /var/www/html/occ config:system:delete trusted_domains 2 2>/dev/null || true - docker exec nextcloud-app php /var/www/html/occ config:system:set trusted_domains 0 --value="localhost" - docker exec nextcloud-app php /var/www/html/occ config:system:set trusted_domains 1 --value="$VM_IP" - docker exec nextcloud-app php /var/www/html/occ config:system:set trusted_domains 2 --value="${VM_IP}:8082" - docker exec nextcloud-app php /var/www/html/occ config:system:set trusted_domains 3 --value="localhost:8082" - docker restart nextcloud-app - echo " โœ… Nextcloud fixed" +if container_is_healthy nextcloud-app; then + echo " โญ๏ธ Nextcloud is healthy, skipping fix" +else + if docker ps | grep -q nextcloud-app; then + docker exec nextcloud-app php /var/www/html/occ config:system:delete trusted_domains 1 2>/dev/null || true + docker exec nextcloud-app php /var/www/html/occ config:system:delete trusted_domains 2 2>/dev/null || true + docker exec nextcloud-app php /var/www/html/occ config:system:set trusted_domains 0 --value="localhost" + docker exec nextcloud-app php /var/www/html/occ config:system:set trusted_domains 1 --value="$VM_IP" + docker exec nextcloud-app php /var/www/html/occ config:system:set trusted_domains 2 --value="${VM_IP}:8082" + docker exec nextcloud-app php /var/www/html/occ config:system:set trusted_domains 3 --value="localhost:8082" + docker restart nextcloud-app + + # Fix permissions for oc_admin on nextcloud tables + docker exec nextcloud-postgres psql -U nextcloud -d nextcloud -c " + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO oc_admin; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO oc_admin; + " 2>/dev/null || true + docker restart nextcloud-app + + echo " โœ… Nextcloud trusted domains fixed" + else + echo " โš ๏ธ nextcloud-app not running" + fi fi # ---- MAUTIC ---- echo "" echo "๐Ÿ“Œ Mautic โ€” Config + admin password..." -if docker ps | grep -q mautic-app; then - # Create config file - cat > /tmp/local.php << EOF +if container_is_healthy mautic-app; then + echo " โญ๏ธ Mautic is healthy, skipping fix" +else + if docker ps | grep -q mautic-app; then + cat > /tmp/mautic-local.php << EOF 'pdo_mysql', - 'db_host' => 'mautic-mariadb', - 'db_port' => '3306', - 'db_name' => 'mautic', - 'db_user' => 'mautic', - 'db_password' => 'mautic123', - 'site_url' => 'http://${VM_IP}:8081' + 'db_driver' => 'pdo_mysql', + 'db_host' => 'mautic-mariadb', + 'db_port' => '3306', + 'db_name' => 'mautic', + 'db_user' => 'mautic', + 'db_password' => 'mautic123', + 'db_table_prefix' => '', + 'site_url' => 'http://${VM_IP}:8081' ); EOF - docker cp /tmp/local.php mautic-app:/var/www/html/config/local.php 2>/dev/null - docker exec mautic-app touch /var/www/html/var/.installed 2>/dev/null - docker exec mautic-app chown -R www-data:www-data /var/www/html/var 2>/dev/null - docker exec mautic-app chown -R www-data:www-data /var/www/html/config 2>/dev/null - docker restart mautic-app - sleep 10 - HASH=$(docker exec mautic-app php -r "echo password_hash('Admin!Password123', PASSWORD_BCRYPT);" 2>/dev/null) - if [ ! -z "$HASH" ]; then - docker exec mautic-mariadb mysql -uroot -pmautic_root_password -e "USE mautic; UPDATE users SET password = '$HASH' WHERE username = 'admin';" 2>/dev/null - echo " โœ… Admin password reset to: Admin!Password123" + docker cp /tmp/mautic-local.php mautic-app:/var/www/html/config/local.php + docker exec mautic-app touch /var/www/html/var/.installed 2>/dev/null || true + docker exec mautic-app chown -R www-data:www-data /var/www/html/var /var/www/html/config 2>/dev/null || true + docker restart mautic-app + sleep 10 + + HASH=$(docker exec mautic-app php -r "echo password_hash('Admin!Password123', PASSWORD_BCRYPT);" 2>/dev/null || true) + if [ -n "$HASH" ]; then + docker exec mautic-mariadb mysql -uroot -pmautic_root_password \ + -e "USE mautic; UPDATE users SET password = '$HASH' WHERE username = 'admin';" 2>/dev/null || true + echo " โœ… Admin password โ†’ Admin!Password123" + fi + docker restart mautic-app + echo " โœ… Mautic fixed โ†’ http://${VM_IP}:8081/s/login" + else + echo " โš ๏ธ mautic-app not running" fi - docker restart mautic-app - echo " โœ… Mautic fixed โ†’ http://${VM_IP}:8081 (admin/Admin!Password123)" -else - echo " โš ๏ธ mautic-app not running" fi # ---- ODOO ---- echo "" echo "๐Ÿ“Œ Odoo โ€” Assets + DB user..." -if docker ps | grep -q odoo-clean-db-1; then - docker exec odoo-clean-db-1 psql -U odoo -d odoo -c "DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';" 2>/dev/null - docker exec odoo-clean-db-1 psql -U odoo -c "ALTER USER odoo WITH PASSWORD 'odoo';" 2>/dev/null - docker exec odoo-clean-odoo-1 bash -c "grep -q 'filestore_check_missing' /etc/odoo/odoo.conf || echo 'filestore_check_missing = False' >> /etc/odoo/odoo.conf" 2>/dev/null - docker restart odoo-clean-odoo-1 - echo " โœ… Odoo fixed โ†’ http://${VM_IP}:8069/web" +if container_is_healthy odoo-clean-odoo-1; then + echo " โญ๏ธ Odoo is healthy, skipping fix" +else + if docker ps | grep -q odoo-clean-db-1; then + docker exec odoo-clean-db-1 psql -U odoo -d odoo \ + -c "DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';" 2>/dev/null || true + docker exec odoo-clean-db-1 psql -U odoo \ + -c "ALTER USER odoo WITH PASSWORD 'odoo';" 2>/dev/null || true + docker exec odoo-clean-odoo-1 bash -c \ + "grep -q 'filestore_check_missing' /etc/odoo/odoo.conf || echo 'filestore_check_missing = False' >> /etc/odoo/odoo.conf" 2>/dev/null || true + docker restart odoo-clean-odoo-1 + echo " โœ… Odoo fixed โ†’ http://${VM_IP}:8069/web" + else + echo " โš ๏ธ odoo-clean-db-1 not running" + fi fi # ---- FRAPPE ---- echo "" -echo "๐Ÿ“Œ Frappe โ€” Full fix..." -if docker ps | grep -q frappe-erpnext && docker ps | grep -q frappe-mariadb; then - DB_NAME=$(docker exec frappe-erpnext cat /home/frappe/frappe-bench/sites/erpnext.navitrends.ovh/site_config.json 2>/dev/null | grep -o '"db_name": *"[^"]*"' | cut -d'"' -f4) - DB_PASS=$(docker exec frappe-erpnext cat /home/frappe/frappe-bench/sites/erpnext.navitrends.ovh/site_config.json 2>/dev/null | grep -o '"db_password": *"[^"]*"' | cut -d'"' -f4) - - if [ -n "$DB_NAME" ] && [ -n "$DB_PASS" ]; then - echo " DB: $DB_NAME" - docker exec frappe-mariadb mysql -uroot -p123 -e " - GRANT ALL PRIVILEGES ON *.* TO '${DB_NAME}'@'%' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION; - GRANT ALL PRIVILEGES ON *.* TO '${DB_NAME}'@'172.%' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION; - GRANT ALL PRIVILEGES ON *.* TO '${DB_NAME}'@'localhost' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION; - FLUSH PRIVILEGES; - " 2>/dev/null && echo " โœ… DB permissions fixed" - - docker exec frappe-erpnext bash -c " - cd /home/frappe/frappe-bench - if [ ! -d 'apps/hrms' ]; then - bench get-app --branch version-15 https://github.com/frappe/hrms - fi - bench --site erpnext.navitrends.ovh install-app hrms - " 2>/dev/null && echo " โœ… HRMS installed" - - docker exec frappe-erpnext bash -c " - cd /home/frappe/frappe-bench - bench --site erpnext.navitrends.ovh set-config redis_cache 'redis://frappe-redis:6379' - bench --site erpnext.navitrends.ovh set-config redis_queue 'redis://frappe-redis:6379' - bench --site erpnext.navitrends.ovh set-config enable_scheduler 1 - bench --site erpnext.navitrends.ovh set-config site_url 'http://${VM_IP}:8080' - bench --site erpnext.navitrends.ovh migrate - bench --site erpnext.navitrends.ovh clear-cache - bench use erpnext.navitrends.ovh - " 2>/dev/null && echo " โœ… Frappe configured" - - # Start bench serve in background inside container - docker exec -d frappe-erpnext bash -c "cd /home/frappe/frappe-bench && nohup bench serve --port 8000 > /tmp/bench.log 2>&1 &" - docker restart frappe-erpnext - echo " โœ… Frappe fixed โ†’ http://${VM_IP}:8080" +echo "๐Ÿ“Œ Frappe โ€” DB permissions + cache + URL..." +if container_is_healthy frappe-erpnext; then + echo " โญ๏ธ Frappe is healthy, skipping fix" +else + if docker ps | grep -q frappe-erpnext && docker ps | grep -q frappe-mariadb; then + SITE_CONFIG="/home/frappe/frappe-bench/sites/erpnext.navitrends.ovh/site_config.json" + DB_NAME=$(docker exec frappe-erpnext cat "$SITE_CONFIG" 2>/dev/null \ + | grep -o '"db_name": *"[^"]*"' | cut -d'"' -f4) + DB_PASS=$(docker exec frappe-erpnext cat "$SITE_CONFIG" 2>/dev/null \ + | grep -o '"db_password": *"[^"]*"' | cut -d'"' -f4) + + if [ -n "$DB_NAME" ] && [ -n "$DB_PASS" ]; then + echo " DB: $DB_NAME" + + docker exec frappe-mariadb mysql -uroot -p123 -e " + GRANT ALL PRIVILEGES ON *.* TO '${DB_NAME}'@'%' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION; + GRANT ALL PRIVILEGES ON *.* TO '${DB_NAME}'@'172.%' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION; + FLUSH PRIVILEGES; + " 2>/dev/null && echo " โœ… DB permissions fixed" || echo " โš ๏ธ DB grant failed" + + docker exec frappe-erpnext bash -c " + cd /home/frappe/frappe-bench + bench --site erpnext.navitrends.ovh set-config redis_cache 'redis://frappe-redis:6379' + bench --site erpnext.navitrends.ovh set-config redis_queue 'redis://frappe-redis:6379' + bench --site erpnext.navitrends.ovh set-config enable_scheduler 1 + bench --site erpnext.navitrends.ovh set-config site_url 'http://${VM_IP}:8080' + bench --site erpnext.navitrends.ovh migrate + bench --site erpnext.navitrends.ovh clear-cache + " 2>/dev/null && echo " โœ… Frappe configured" || echo " โš ๏ธ Frappe config step had errors" + + docker restart frappe-erpnext + echo " โœ… Frappe fixed โ†’ http://${VM_IP}:8080" + else + echo " โš ๏ธ Could not read Frappe DB credentials" + fi else - echo " โš ๏ธ Could not get Frappe credentials" + echo " โš ๏ธ Frappe containers not running" fi fi # ---- N8N ---- echo "" echo "๐Ÿ“Œ n8n โ€” Network check..." -docker network inspect integration-network &>/dev/null || docker network create integration-network -echo " โœ… n8n โ†’ http://${VM_IP}:5678" +if container_is_healthy n8n-app; then + echo " โญ๏ธ n8n is healthy, skipping fix" +else + docker network inspect integration-network &>/dev/null \ + || docker network create integration-network && echo " โœ… Created integration-network" + + if ! docker ps | grep -q n8n-app; then + cd "$SCRIPT_DIR/compose-files/n8n-setup" && docker-compose up -d && cd "$SCRIPT_DIR" + fi + echo " โœ… n8n โ†’ http://${VM_IP}:5678" +fi # -------------------------------------------------- # Summary @@ -276,7 +402,7 @@ echo "=========================================" echo "โœ… RESTORE COMPLETE" echo "=========================================" echo " Nextcloud โ†’ http://${VM_IP}:8082" -echo " Mautic โ†’ http://${VM_IP}:8081 (admin/Admin!Password123)" +echo " Mautic โ†’ http://${VM_IP}:8081/s/login (admin / Admin!Password123)" echo " Odoo โ†’ http://${VM_IP}:8069/web" echo " n8n โ†’ http://${VM_IP}:5678" echo " Frappe โ†’ http://${VM_IP}:8080"