Files
CloudOps/platform/restore-myapps.sh
2026-06-05 17:37:45 +01:00

485 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# =============================================
# 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 <IP> <USER> [--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
# - Works inside Docker containers (no ip command needed)
# - Supports both docker-compose and docker compose
# =============================================
set -uo pipefail
# --------------------------------------------------
# Compatibility fixes — must come first
# --------------------------------------------------
# docker compose v2 compat (fixes "docker-compose: command not found")
if ! command -v docker-compose &>/dev/null; then
if docker compose version &>/dev/null 2>&1; then
docker-compose() { docker compose "$@"; }
export -f docker-compose
fi
fi
# --------------------------------------------------
# Parse arguments
# --------------------------------------------------
REMOTE_MODE=false
REMOTE_IP=""
REMOTE_USER="root"
SSH_KEY=""
SSH_PASSWORD=""
USE_PASSWORD=false
while [[ $# -gt 0 ]]; do
case "$1" in
--remote)
REMOTE_MODE=true
REMOTE_IP="$2"
REMOTE_USER="${3:-root}"
shift 3
;;
--key)
SSH_KEY="$2"
shift 2
;;
--password)
USE_PASSWORD=true
shift
;;
*)
shift
;;
esac
done
# --------------------------------------------------
# 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 <IP> [USER] [--key /path/key]"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REMOTE_DEST="/tmp/restore-session-$(date +%s)"
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=15"
if [ -n "$SSH_KEY" ]; then
SSH_OPTS="$SSH_OPTS -i $SSH_KEY"
elif [ "$USE_PASSWORD" = true ]; then
if ! command -v sshpass &>/dev/null; then
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"
SCP_CMD="sshpass -p '$SSH_PASS' 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 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 "========================================="
echo "✅ Remote restore complete on $REMOTE_IP"
echo "========================================="
exit 0
fi
# ===================================================
# LOCAL RESTORE (runs directly on the target machine)
# ===================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# --------------------------------------------------
# Detect IP — works inside containers too
# --------------------------------------------------
VM_IP=""
# Try hostname -I first (works inside containers)
if command -v hostname &>/dev/null; then
VM_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
# Fallback: try ip command
if [ -z "$VM_IP" ] && command -v ip &>/dev/null; then
VM_IP=$(ip -4 addr show 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v 127.0.0.1 | head -1)
fi
# Fallback: try /proc/net/fib_trie (works in minimal containers)
if [ -z "$VM_IP" ] && [ -f /proc/net/fib_trie ]; then
VM_IP=$(awk '/32 HOST/{print f} {f=$2}' /proc/net/fib_trie 2>/dev/null | grep -v 127.0.0.1 | head -1)
fi
# Final fallback: use known main server IP
if [ -z "$VM_IP" ]; then
VM_IP="173.249.20.244"
echo " ⚠️ Could not detect IP, defaulting to $VM_IP"
fi
echo "========================================="
echo "🔄 Smart Restore — LOCAL MODE"
echo " Machine IP: $VM_IP"
echo " Backup dir: $SCRIPT_DIR"
echo "========================================="
# --------------------------------------------------
# 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 "========================================="
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-postgres"
["odoo-clean_db-data"]="odoo-clean-db-1"
["odoo-clean_odoo-etc"]="odoo-clean-odoo-1"
)
if [ -d "$SCRIPT_DIR/volumes" ]; then
cd "$SCRIPT_DIR/volumes"
for backup in *.tar.gz; do
[ -f "$backup" ] || { echo " ⚠️ No volume backups found in $SCRIPT_DIR/volumes"; break; }
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:ro" \
alpine \
sh -c "cd /target && tar xzf /backup/${backup}" \
&& echo "✅" || echo "⚠️ FAILED"
done
cd "$SCRIPT_DIR"
else
echo " ⚠️ No volumes/ directory found in backup — skipping volume restore"
fi
# --------------------------------------------------
# 2. Start containers — skip healthy ones
# --------------------------------------------------
echo ""
echo "========================================="
echo "🚀 STEP 2 — Starting Containers"
echo "========================================="
declare -A APP_DIRS=(
["Frappe"]="frappe-setup"
["Odoo"]="odoo-clean"
["Nextcloud"]="nextcloud-setup"
["Mautic"]="mautic-setup"
["n8n"]="n8n-setup"
)
declare -A APP_MAIN_CONTAINER=(
["Frappe"]="frappe-erpnext"
["Odoo"]="odoo-clean-odoo-1"
["Nextcloud"]="nextcloud-app"
["Mautic"]="mautic-app"
["n8n"]="n8n-app"
)
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]}"
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"
else
echo " ⚠️ No compose-files/ directory found — skipping container start"
fi
echo ""
echo "⏳ Waiting 60s for containers to initialize..."
sleep 60
# --------------------------------------------------
# 3. Post-restore fixes
# --------------------------------------------------
echo ""
echo "========================================="
echo "🔧 STEP 3 — Applying Post-Restore Fixes"
echo "========================================="
# ---- NEXTCLOUD ----
echo ""
echo "📌 Nextcloud — Trusted domains..."
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 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
<?php
\$parameters = array(
'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/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
fi
# ---- ODOO ----
echo ""
echo "📌 Odoo — Assets + DB user..."
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 — 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 " ⚠️ Frappe containers not running"
fi
fi
# ---- N8N ----
echo ""
echo "📌 n8n — Network + volumes check..."
if container_is_healthy n8n-app; then
echo " ⏭️ n8n is healthy, skipping fix"
else
# Ensure network exists
docker network inspect integration-network &>/dev/null \
|| { docker network create integration-network && echo " ✅ Created integration-network"; }
# Check if n8n volumes exist in backup — restore them if not already done
N8N_VOLUMES=("n8n-setup_n8n-data" "n8n-setup_n8n-db-data")
for vol in "${N8N_VOLUMES[@]}"; do
vol_backup="$SCRIPT_DIR/volumes/${vol}.tar.gz"
if [ -f "$vol_backup" ]; then
echo -n " 📁 Restoring $vol ... "
docker volume create "$vol" &>/dev/null || true
docker run --rm \
-v "${vol}:/target" \
-v "$SCRIPT_DIR/volumes:/backup:ro" \
alpine \
sh -c "cd /target && tar xzf /backup/${vol}.tar.gz" \
&& echo "✅" || echo "⚠️ FAILED"
else
echo " ⚠️ $vol backup not found in this archive — n8n will start fresh"
docker volume create "$vol" &>/dev/null || true
fi
done
# Start n8n via compose if compose file exists
N8N_COMPOSE_DIR="$SCRIPT_DIR/compose-files/n8n-setup"
if [ -d "$N8N_COMPOSE_DIR" ]; then
cd "$N8N_COMPOSE_DIR"
docker compose up -d 2>&1 | tail -3
cd "$SCRIPT_DIR"
else
# Fallback: check if n8n-app container exists but is stopped
if docker ps -a | grep -q n8n-app; then
docker start n8n-app && echo " ✅ Started existing n8n-app container"
else
echo " ⚠️ No compose file and no existing n8n-app container found"
echo " Start manually: docker start n8n-app"
fi
fi
echo " ✅ n8n → http://${VM_IP}:5678"
fi
# --------------------------------------------------
# Summary
# --------------------------------------------------
echo ""
echo "========================================="
echo "✅ RESTORE COMPLETE"
echo "========================================="
echo " Nextcloud → http://${VM_IP}:8082"
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"
echo "========================================="