diff --git a/scripts/deploy-platform-to-server.sh b/scripts/deploy-platform-to-server.sh new file mode 100755 index 0000000..4065f9b --- /dev/null +++ b/scripts/deploy-platform-to-server.sh @@ -0,0 +1,141 @@ +#!/bin/bash +# ───────────────────────────────────────────────────────────── +# deploy-platform-to-server.sh +# Run from the VM (backup server) to push the latest platform +# version to a remote server (e.g. main server after failure). +# Usage: +# ./deploy-platform-to-server.sh # deploys to default main server +# ./deploy-platform-to-server.sh 192.168.1.50 # deploys to custom IP +# ./deploy-platform-to-server.sh 192.168.1.50 2222 # custom IP + SSH port +# ───────────────────────────────────────────────────────────── + +LOCAL_BACKUP_DIR="/backups/platform" +DEPLOY_DIR="/root/management-platform" +LOG="/root/deploy-to-server.log" + +# ── Target server (override via args) ──────────────────────── +TARGET_IP="${1:-173.249.20.244}" +TARGET_PORT="${2:-22}" +TARGET_USER="root" +SSH_KEY="/root/.ssh/contabo-key" # adjust if different on VM + +SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=15 -p $TARGET_PORT -i $SSH_KEY" +SCP_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=15 -P $TARGET_PORT -i $SSH_KEY" + +log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG"; } + +log "════════════════════════════════════════════════" +log "🚀 Deploy platform → $TARGET_USER@$TARGET_IP:$TARGET_PORT" +log "════════════════════════════════════════════════" + +# ── Find latest local platform backup ──────────────────────── +LATEST=$(ls -t "$LOCAL_BACKUP_DIR"/platform-backup-*.tar.gz 2>/dev/null | head -1) + +if [ -z "$LATEST" ]; then + log "❌ No local platform backups found in $LOCAL_BACKUP_DIR" + log " Run pull-platform-backup.sh first" + exit 1 +fi + +BACKUP_NAME=$(basename "$LATEST") +log "📦 Using backup: $BACKUP_NAME" + +# ── Test SSH connection ─────────────────────────────────────── +log "🔗 Testing SSH connection to $TARGET_IP:$TARGET_PORT ..." +ssh $SSH_OPTS $TARGET_USER@$TARGET_IP "echo ok" > /dev/null 2>&1 +if [ $? -ne 0 ]; then + log "❌ Cannot connect to $TARGET_IP:$TARGET_PORT" + log " Check: SSH key at $SSH_KEY, server is up, port is correct" + exit 1 +fi +log "✅ SSH connection OK" + +# ── Extract backup locally to get the platform files ───────── +EXTRACT_DIR=$(mktemp -d /tmp/deploy-to-server-XXXXXX) +log "📂 Extracting backup locally..." +tar -xzf "$LATEST" -C "$EXTRACT_DIR" + +if [ $? -ne 0 ]; then + log "❌ Extraction failed — backup may be corrupt" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +PLATFORM_SRC=$(find "$EXTRACT_DIR" -maxdepth 3 -type d -name "management-platform" | head -1) + +if [ -z "$PLATFORM_SRC" ] || [ ! -f "$PLATFORM_SRC/app.py" ]; then + log "❌ Could not find valid management-platform/app.py in archive" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +log "✅ Platform source found: $PLATFORM_SRC" + +# ── Stop the service on remote if running ──────────────────── +log "🛑 Stopping service on remote (if running)..." +ssh $SSH_OPTS $TARGET_USER@$TARGET_IP \ + "systemctl stop management-platform 2>/dev/null ; pkill -f 'flask\|app.py' 2>/dev/null ; echo stopped" + +# ── Backup current version on remote ───────────────────────── +log "📦 Archiving current version on remote..." +ssh $SSH_OPTS $TARGET_USER@$TARGET_IP \ + "if [ -d /root/management-platform ] && [ \"\$(ls -A /root/management-platform 2>/dev/null)\" ]; then + mkdir -p /backups/platform/old-deploys + tar -czf /backups/platform/old-deploys/management-platform.old.\$(date +%Y%m%d_%H%M%S).tar.gz \ + -C /root management-platform 2>/dev/null + rm -rf /root/management-platform + ls -t /backups/platform/old-deploys/management-platform.old.*.tar.gz 2>/dev/null \ + | tail -n +3 | xargs rm -f 2>/dev/null + echo 'archived' + else + echo 'nothing to archive' + fi" + +# ── Copy platform files to remote ──────────────────────────── +log "📤 Copying platform files to $TARGET_IP ..." +scp $SCP_OPTS -r "$PLATFORM_SRC" $TARGET_USER@$TARGET_IP:/root/management-platform + +if [ $? -ne 0 ]; then + log "❌ SCP failed" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +log "✅ Files copied" + +# ── Fix permissions and venv on remote ─────────────────────── +log "🔧 Fixing permissions and venv on remote..." +ssh $SSH_OPTS $TARGET_USER@$TARGET_IP " + chmod +x /root/management-platform/*.sh 2>/dev/null + chmod +x /root/management-platform/*.py 2>/dev/null + cd /root/management-platform + if [ ! -d venv ]; then + echo '🐍 Creating venv...' + python3 -m venv venv + source venv/bin/activate + pip install flask --quiet + deactivate + else + echo '🐍 Venv exists' + fi +" + +# ── Start service on remote ─────────────────────────────────── +log "▶️ Starting management-platform on remote..." +ssh $SSH_OPTS $TARGET_USER@$TARGET_IP "systemctl start management-platform" + +sleep 4 + +STATUS=$(ssh $SSH_OPTS $TARGET_USER@$TARGET_IP "systemctl is-active management-platform 2>/dev/null") + +if [ "$STATUS" = "active" ]; then + log "✅ Service is running on $TARGET_IP" + log "🌐 Access at: http://$TARGET_IP:5000" +else + log "⚠️ Service may not be running (status: $STATUS)" + log " Check on remote: journalctl -u management-platform -n 30" +fi + +# ── Cleanup ─────────────────────────────────────────────────── +rm -rf "$EXTRACT_DIR" +log "🎉 Done — platform deployed to $TARGET_IP from backup: $BACKUP_NAME" diff --git a/scripts/deploy-platform.sh b/scripts/deploy-platform.sh new file mode 100755 index 0000000..4e877bc --- /dev/null +++ b/scripts/deploy-platform.sh @@ -0,0 +1,218 @@ +#!/bin/bash +# ───────────────────────────────────────────────────────────── +# deploy-platform.sh +# Deploys the latest locally-pulled platform backup. +# Self-healing: installs all required system dependencies. +# Only redeploys if the backup is newer than the running version. +# Run every hour (offset ~5min after pull) via cron. +# ───────────────────────────────────────────────────────────── + +LOCAL_BACKUP_DIR="/backups/platform" +DEPLOY_DIR="/root/management-platform" +OLD_DEPLOY_DIR="/backups/platform/old-deploys" +STAMP_FILE="/backups/platform/.last-deployed" +SERVICE_FILE="/etc/systemd/system/management-platform.service" +LOG="/root/deploy-platform.log" + +log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG"; } + +mkdir -p "$OLD_DEPLOY_DIR" + +# ════════════════════════════════════════════════════════════ +# STEP 1 — Check & install system dependencies +# ════════════════════════════════════════════════════════════ +log "🔍 Checking system dependencies..." + +MISSING_PKGS="" + +# python3 +if ! command -v python3 &>/dev/null; then + MISSING_PKGS="$MISSING_PKGS python3" +fi + +# python3-venv (needed for venv creation) +PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null) +if ! python3 -c "import ensurepip" &>/dev/null; then + MISSING_PKGS="$MISSING_PKGS python${PYTHON_VERSION}-venv" +fi + +# sshpass (used by restore feature) +if ! command -v sshpass &>/dev/null; then + MISSING_PKGS="$MISSING_PKGS sshpass" +fi + +if [ -n "$MISSING_PKGS" ]; then + log "📦 Installing missing packages:$MISSING_PKGS" + apt-get update -qq + apt-get install -y --quiet $MISSING_PKGS + if [ $? -ne 0 ]; then + log "❌ apt install failed — check internet/apt on this machine" + exit 1 + fi + log "✅ System packages installed" +else + log "✅ System dependencies OK" +fi + +# ════════════════════════════════════════════════════════════ +# STEP 2 — Find latest local backup +# ════════════════════════════════════════════════════════════ +LATEST=$(ls -t "$LOCAL_BACKUP_DIR"/platform-backup-*.tar.gz 2>/dev/null | head -1) + +if [ -z "$LATEST" ]; then + log "❌ No local platform backups found in $LOCAL_BACKUP_DIR" + log " Run pull-platform-backup.sh first" + exit 1 +fi + +BACKUP_NAME=$(basename "$LATEST") + +# ── Check if already deployed ───────────────────────────────── +if [ -f "$STAMP_FILE" ] && [ "$(cat $STAMP_FILE)" = "$BACKUP_NAME" ]; then + log "✅ Already running latest: $BACKUP_NAME — nothing to do" + exit 0 +fi + +log "🚀 New backup detected: $BACKUP_NAME — deploying..." + +# ════════════════════════════════════════════════════════════ +# STEP 3 — Extract & validate +# ════════════════════════════════════════════════════════════ +EXTRACT_DIR=$(mktemp -d /tmp/platform-deploy-XXXXXX) +log "📂 Extracting to $EXTRACT_DIR ..." + +tar -xzf "$LATEST" -C "$EXTRACT_DIR" +if [ $? -ne 0 ]; then + log "❌ tar extraction failed — backup may be corrupt: $BACKUP_NAME" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +# Find management-platform/ inside archive (handles any nesting level) +PLATFORM_SRC=$(find "$EXTRACT_DIR" -maxdepth 3 -type d -name "management-platform" | head -1) + +if [ -z "$PLATFORM_SRC" ]; then + log "❌ Could not find management-platform/ inside archive" + log " Archive contents:" + find "$EXTRACT_DIR" -maxdepth 3 | head -20 | tee -a "$LOG" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +if [ ! -f "$PLATFORM_SRC/app.py" ]; then + log "❌ app.py not found in extracted platform — archive looks wrong" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +log "✅ Found platform at: $PLATFORM_SRC" + +# Strip venv and cache from source — always rebuild locally +rm -rf "$PLATFORM_SRC/venv" "$PLATFORM_SRC/__pycache__" +find "$PLATFORM_SRC" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null + +# ════════════════════════════════════════════════════════════ +# STEP 4 — Stop service & kill strays +# ════════════════════════════════════════════════════════════ +log "🛑 Stopping management-platform service..." +systemctl stop management-platform 2>/dev/null +sleep 2 +pkill -f "python.*app.py" 2>/dev/null +sleep 1 + +# ════════════════════════════════════════════════════════════ +# STEP 5 — Archive current deploy, swap in new one +# ════════════════════════════════════════════════════════════ +if [ -d "$DEPLOY_DIR" ] && [ "$(ls -A $DEPLOY_DIR 2>/dev/null)" ]; then + OLD_NAME="management-platform.old.$(date +%Y%m%d_%H%M%S)" + log "📦 Archiving current deploy → $OLD_DEPLOY_DIR/$OLD_NAME.tar.gz" + tar -czf "$OLD_DEPLOY_DIR/$OLD_NAME.tar.gz" -C /root management-platform 2>/dev/null + rm -rf "$DEPLOY_DIR" + # Keep only last 2 old deploys + ls -t "$OLD_DEPLOY_DIR"/management-platform.old.*.tar.gz 2>/dev/null \ + | tail -n +3 | xargs rm -f 2>/dev/null +fi + +# Remove any leftover .old directories in /root from old scripts +rm -rf /root/management-platform.old.* 2>/dev/null + +log "📁 Deploying new version..." +cp -r "$PLATFORM_SRC" "$DEPLOY_DIR" + +chmod +x "$DEPLOY_DIR"/*.sh 2>/dev/null +chmod +x "$DEPLOY_DIR"/*.py 2>/dev/null + +# ════════════════════════════════════════════════════════════ +# STEP 6 — Build fresh venv & install flask +# ════════════════════════════════════════════════════════════ +log "🐍 Building fresh Python venv..." +cd "$DEPLOY_DIR" + +python3 -m venv venv +if [ $? -ne 0 ]; then + log "❌ venv creation failed" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +# Bootstrap pip in case ensurepip is incomplete +venv/bin/python -m ensurepip --upgrade 2>/dev/null || true +venv/bin/python -m pip install --upgrade pip --quiet 2>/dev/null || true + +log "📦 Installing Flask..." +venv/bin/pip install flask --quiet +if [ $? -ne 0 ]; then + log "❌ Flask install failed" + rm -rf "$EXTRACT_DIR" + exit 1 +fi + +log "✅ Venv ready ($(venv/bin/python --version), flask $(venv/bin/pip show flask | grep Version | cut -d' ' -f2))" + +# ════════════════════════════════════════════════════════════ +# STEP 7 — Write correct systemd service file +# ════════════════════════════════════════════════════════════ +log "🔧 Writing systemd service file..." +cat > "$SERVICE_FILE" << 'SVCEOF' +[Unit] +Description=Navitrends Management Platform +After=network.target docker.target +Wants=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/root/management-platform +ExecStart=/root/management-platform/venv/bin/python /root/management-platform/app.py +Restart=on-failure +RestartSec=10 +StartLimitIntervalSec=60 +StartLimitBurst=3 +StandardOutput=append:/root/management-platform/app.log +StandardError=append:/root/management-platform/app.log + +[Install] +WantedBy=multi-user.target +SVCEOF + +systemctl daemon-reload + +# ════════════════════════════════════════════════════════════ +# STEP 8 — Clean up & start +# ════════════════════════════════════════════════════════════ +rm -rf "$EXTRACT_DIR" + +log "▶️ Starting management-platform service..." +systemctl start management-platform +sleep 4 + +if systemctl is-active --quiet management-platform; then + log "✅ Service is running" + echo "$BACKUP_NAME" > "$STAMP_FILE" + log "🎉 Deploy complete: $BACKUP_NAME" +else + log "❌ Service failed to start. Last app.log lines:" + tail -20 "$DEPLOY_DIR/app.log" | tee -a "$LOG" + log " Full check: journalctl -u management-platform -n 30" + exit 1 +fi diff --git a/scripts/pull-backups.sh b/scripts/pull-backups.sh new file mode 100755 index 0000000..77a363f --- /dev/null +++ b/scripts/pull-backups.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# ============================================= +# pull-backup.sh — Run on VM (local machine) +# Pulls a backup from /backups/main-server/ +# and extracts it ready for restore. +# +# Usage: +# ./pull-backup.sh → list available backups +# ./pull-backup.sh latest → pull the most recent backup +# ./pull-backup.sh myapps-backup-20260311_021129.tar.gz → pull specific one +# ============================================= + +BACKUP_DIR="/backups/main-server" +RESTORE_DIR="/root/restored-backups" + +mkdir -p "$RESTORE_DIR" + +echo "=========================================" +echo "📥 Backup Pull Tool" +echo " Source: $BACKUP_DIR" +echo " Dest: $RESTORE_DIR" +echo "=========================================" + +# -------------------------------------------------- +# No argument → list available backups +# -------------------------------------------------- +if [ -z "${1:-}" ]; then + echo "" + echo "📋 Available backups:" + echo "" + if ls "$BACKUP_DIR"/myapps-backup-*.tar.gz &>/dev/null; then + ls -lht "$BACKUP_DIR"/myapps-backup-*.tar.gz | awk '{print NR". "$NF" ("$5")"}' + else + echo " (no backups found in $BACKUP_DIR)" + fi + echo "" + echo "Usage:" + echo " $0 latest" + echo " $0 myapps-backup-20260311_021129.tar.gz" + exit 0 +fi + +# -------------------------------------------------- +# 'latest' → pick newest file +# -------------------------------------------------- +if [ "$1" = "latest" ]; then + BACKUP_FILE=$(ls -t "$BACKUP_DIR"/myapps-backup-*.tar.gz 2>/dev/null | head -1) + if [ -z "$BACKUP_FILE" ]; then + echo "❌ No backups found in $BACKUP_DIR" + exit 1 + fi + echo "🕐 Latest backup: $(basename $BACKUP_FILE)" +else + # Specific filename given — accept with or without path prefix + BASENAME=$(basename "$1") + BACKUP_FILE="$BACKUP_DIR/$BASENAME" + if [ ! -f "$BACKUP_FILE" ]; then + echo "❌ Not found: $BACKUP_FILE" + echo " Run $0 with no arguments to list available backups." + exit 1 + fi +fi + +BACKUP_NAME=$(basename "$BACKUP_FILE" .tar.gz) +EXTRACT_PATH="$RESTORE_DIR/$BACKUP_NAME" + +# -------------------------------------------------- +# Already extracted? +# -------------------------------------------------- +if [ -d "$EXTRACT_PATH" ]; then + echo "" + echo "⚠️ This backup is already extracted at:" + echo " $EXTRACT_PATH" + echo "" + read -p " Re-extract? (y/N): " CONFIRM + if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then + echo " Skipping extraction." + else + rm -rf "$EXTRACT_PATH" + echo " 🗑️ Removed old extraction." + fi +fi + +BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) +echo "" +echo "📦 Extracting: $(basename $BACKUP_FILE) [$BACKUP_SIZE]" +echo " → $EXTRACT_PATH" +echo "" + +cd "$RESTORE_DIR" +tar -xzf "$BACKUP_FILE" + +if [ $? -ne 0 ]; then + echo "❌ Extraction failed!" + exit 1 +fi + +echo "" +echo "✅ Extraction complete!" +echo "" +echo "📋 Backup contents:" +ls -lh "$EXTRACT_PATH/" +echo "" +echo "=========================================" +echo "🚀 To restore this backup:" +echo "" +echo " # On THIS VM (local restore):" +echo " cd $EXTRACT_PATH" +echo " ./restore-myapps.sh" +echo "" +echo " # On the MAIN SERVER (remote restore via SSH):" +echo " cd $EXTRACT_PATH" +echo " ./restore-myapps.sh --remote [SSH_KEY or PASSWORD]" +echo "=========================================" diff --git a/scripts/pull-platform-backup.sh b/scripts/pull-platform-backup.sh new file mode 100755 index 0000000..5b2650d --- /dev/null +++ b/scripts/pull-platform-backup.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# ───────────────────────────────────────────────────────────── +# pull-platform-backup.sh +# Pulls the latest platform backup from main server to this VM. +# Run every hour via cron. +# ───────────────────────────────────────────────────────────── + +MAIN_SERVER="root@173.249.20.244" +REMOTE_BACKUP_DIR="/root/backups/platform-backups" +LOCAL_BACKUP_DIR="/backups/platform" +LOG="/root/pull-platform.log" + +log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG"; } + +mkdir -p "$LOCAL_BACKUP_DIR" + +log "🔍 Checking latest platform backup on main server..." + +LATEST=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 $MAIN_SERVER \ + "ls -t ${REMOTE_BACKUP_DIR}/platform-backup-*.tar.gz 2>/dev/null | head -1") + +if [ -z "$LATEST" ]; then + log "❌ No platform backups found on main server at ${REMOTE_BACKUP_DIR}" + exit 1 +fi + +BACKUP_NAME=$(basename "$LATEST") +LOCAL_PATH="$LOCAL_BACKUP_DIR/$BACKUP_NAME" + +if [ -f "$LOCAL_PATH" ]; then + log "✅ Already have latest: $BACKUP_NAME — nothing to pull" + exit 0 +fi + +log "📥 Pulling: $BACKUP_NAME ..." +scp -o StrictHostKeyChecking=no -o ConnectTimeout=15 \ + "$MAIN_SERVER:$LATEST" "$LOCAL_PATH" + +if [ $? -ne 0 ]; then + log "❌ SCP failed — check SSH access to main server" + rm -f "$LOCAL_PATH" # remove partial file + exit 1 +fi + +log "✅ Pulled successfully: $BACKUP_NAME" + +# Keep only last 48 backups locally +log "🧹 Cleaning old backups (keeping last 48)..." +ls -t "$LOCAL_BACKUP_DIR"/platform-backup-*.tar.gz 2>/dev/null | tail -n +49 | xargs rm -f 2>/dev/null + +log "Done."