universal zero-downtime migration from legacy WireGuard/Go stack to operator-docker
This commit is contained in:
parent
55cacb0ff4
commit
7d4b9ece19
1 changed files with 235 additions and 0 deletions
235
01-migrate-user.sh
Normal file
235
01-migrate-user.sh
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
#!/bin/bash -eu
|
||||
# =============================================================================
|
||||
# 01-migrate-user.sh — Run as Linux user (not root)
|
||||
# Installs operator-docker and prepares for peer migration
|
||||
# Auto-rollback: restores nginx, certs and legacy stack on failure
|
||||
# =============================================================================
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "[X] Run as your Linux user, not root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RESET='\033[0m'
|
||||
|
||||
BACKUP_DIR="$HOME/.wg-migration"
|
||||
LOG="$BACKUP_DIR/migration.log"
|
||||
REPO_URL="git@git.simplifiedprivacy.com:zenaku/operator-docker.git"
|
||||
OPERATOR_DIR="$HOME/operator-docker"
|
||||
|
||||
ok() { echo -e "${GREEN}[OK]${RESET} $1"; echo "[OK] $1" >> "$LOG"; }
|
||||
info() { echo -e "${CYAN}[i]${RESET} $1"; echo "[i] $1" >> "$LOG"; }
|
||||
warn() { echo -e "${YELLOW}[!]${RESET} $1"; echo "[!] $1" >> "$LOG"; }
|
||||
fail() { echo -e "${RED}[X]${RESET} $1"; echo "[X] $1" >> "$LOG"; exit 1; }
|
||||
sep() { echo -e "\n${CYAN}------------------------------------${RESET}"; }
|
||||
|
||||
rollback_01() {
|
||||
echo ""
|
||||
warn "Installation failed — starting rollback..."
|
||||
echo "[ROLLBACK-01] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG"
|
||||
|
||||
[ -d "$OPERATOR_DIR" ] && rm -rf "$OPERATOR_DIR" && warn "operator-docker removed."
|
||||
|
||||
if [ -d "$BACKUP_DIR/nginx/sites-available" ]; then
|
||||
sudo cp -r "$BACKUP_DIR/nginx/sites-available/." /etc/nginx/sites-available/ 2>/dev/null || true
|
||||
sudo cp -r "$BACKUP_DIR/nginx/sites-enabled/." /etc/nginx/sites-enabled/ 2>/dev/null || true
|
||||
warn "Nginx restored."
|
||||
fi
|
||||
|
||||
if [ -d "$BACKUP_DIR/letsencrypt" ]; then
|
||||
sudo cp -r "$BACKUP_DIR/letsencrypt/." /etc/letsencrypt/ 2>/dev/null || true
|
||||
warn "Certificates restored."
|
||||
fi
|
||||
|
||||
LEGACY_COMPOSE_DIR=$(cat "$BACKUP_DIR/legacy_compose_dir" 2>/dev/null || true)
|
||||
if [ -n "$LEGACY_COMPOSE_DIR" ] && [ -d "$LEGACY_COMPOSE_DIR" ]; then
|
||||
sudo modprobe ip6table_filter 2>/dev/null || true
|
||||
cd "$LEGACY_COMPOSE_DIR"
|
||||
docker compose up -d 2>/dev/null || true
|
||||
sleep 5
|
||||
LEGACY_UP=$(docker ps --format '{{.Names}}' | grep -i wireguard | head -1 || true)
|
||||
[ -n "$LEGACY_UP" ] && ok "Legacy restored: $LEGACY_UP" || \
|
||||
warn "Legacy failed to start. Run manually: cd $LEGACY_COMPOSE_DIR && docker compose up -d"
|
||||
fi
|
||||
|
||||
sudo systemctl start nginx 2>/dev/null || true
|
||||
warn "Rollback complete. Check: cat $LOG"
|
||||
}
|
||||
trap rollback_01 ERR
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " 01 - operator-docker installation"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "========================================="
|
||||
echo "[START-01] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG"
|
||||
|
||||
# ─── PREREQUISITES ────────────────────────────────────────────────────────────
|
||||
sep
|
||||
info "Checking prerequisites..."
|
||||
|
||||
[ -f "$BACKUP_DIR/private_key" ] || fail "No backup found. Run: sudo bash 00-migrate-root.sh"
|
||||
[ -f "$BACKUP_DIR/wg0-legacy.conf" ] || fail "No peer backup found. Run: sudo bash 00-migrate-root.sh"
|
||||
[ -f "$BACKUP_DIR/legacy_compose_dir" ] || fail "No legacy snapshot found. Run: sudo bash 00-migrate-root.sh"
|
||||
ok "Backups found."
|
||||
|
||||
# ─── CLEAN FAILED INSTALL ─────────────────────────────────────────────────────
|
||||
sep
|
||||
info "Checking for previous installation..."
|
||||
|
||||
_env_is_corrupt() {
|
||||
local env_file="$1"
|
||||
[ ! -f "$env_file" ] && return 1
|
||||
|
||||
local base_domain
|
||||
base_domain=$(grep -E '^BASE_DOMAIN=' "$env_file" 2>/dev/null \
|
||||
| head -1 | cut -d= -f2- | tr -d '[:space:]' || true)
|
||||
|
||||
[ -z "$base_domain" ] && { warn ".env: BASE_DOMAIN is empty."; return 0; }
|
||||
|
||||
echo "$base_domain" | grep -qE '[/;|&<> ]' && {
|
||||
warn ".env: BASE_DOMAIN contains invalid characters: '$base_domain'"; return 0; }
|
||||
|
||||
echo "$base_domain" | grep -qE '^[0-9]+-|^ash' && {
|
||||
warn ".env: BASE_DOMAIN looks like a script name: '$base_domain'"; return 0; }
|
||||
|
||||
[ "${#base_domain}" -gt 253 ] && {
|
||||
warn ".env: BASE_DOMAIN too long (${#base_domain} chars)."; return 0; }
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_purge_failed_install() {
|
||||
warn "Cleaning up failed installation..."
|
||||
echo "[PURGE] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG"
|
||||
|
||||
if [ -f "$OPERATOR_DIR/docker-compose.yml" ]; then
|
||||
cd "$OPERATOR_DIR"
|
||||
docker compose down --remove-orphans 2>/dev/null && warn "Previous stack stopped." || true
|
||||
cd "$HOME"
|
||||
fi
|
||||
|
||||
local project
|
||||
project=$(basename "$OPERATOR_DIR" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-')
|
||||
while IFS= read -r cname; do
|
||||
docker rm -f "$cname" 2>/dev/null && warn "Container removed: $cname" || true
|
||||
done < <(docker ps -a --format '{{.Names}}' | grep -i "^${project}" 2>/dev/null || true)
|
||||
|
||||
rm -rf "$OPERATOR_DIR"
|
||||
ok "Directory $OPERATOR_DIR removed."
|
||||
}
|
||||
|
||||
if [ -d "$OPERATOR_DIR" ]; then
|
||||
if _env_is_corrupt "$OPERATOR_DIR/.env"; then
|
||||
warn "Corrupt .env detected from previous attempt."
|
||||
_purge_failed_install
|
||||
elif [ ! -f "$OPERATOR_DIR/docker-compose.yml" ]; then
|
||||
warn "Incomplete repo (missing docker-compose.yml). Cleaning..."
|
||||
_purge_failed_install
|
||||
else
|
||||
warn "Existing directory found. Removing for fresh install..."
|
||||
_purge_failed_install
|
||||
fi
|
||||
else
|
||||
info "No previous installation found."
|
||||
fi
|
||||
|
||||
# ─── SSH KEY ──────────────────────────────────────────────────────────────────
|
||||
sep
|
||||
info "Checking SSH key..."
|
||||
[ -f "$HOME/.ssh/id_ed25519" ] || fail "SSH key not found. Run: sudo bash 00-migrate-root.sh"
|
||||
ok "SSH key found."
|
||||
|
||||
info "Checking access to private repo..."
|
||||
SSH_TEST=$(GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10' \
|
||||
ssh -T git@git.simplifiedprivacy.com 2>&1 || true)
|
||||
|
||||
if echo "$SSH_TEST" | grep -qiE "Welcome|successfully authenticated"; then
|
||||
ok "Repo access authorized."
|
||||
else
|
||||
echo ""
|
||||
warn "Could not authenticate with private repo."
|
||||
echo ""
|
||||
echo " Send this SSH key to Simplified Privacy:"
|
||||
echo ""
|
||||
cat "$HOME/.ssh/id_ed25519.pub"
|
||||
echo ""
|
||||
fail "Once authorized, run again: bash 01-migrate-user.sh"
|
||||
fi
|
||||
|
||||
# ─── CLONE REPO ───────────────────────────────────────────────────────────────
|
||||
sep
|
||||
info "Cloning operator-docker repository..."
|
||||
|
||||
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' \
|
||||
git clone "$REPO_URL" "$OPERATOR_DIR" || fail "Could not clone repository."
|
||||
ok "Repository cloned to $OPERATOR_DIR"
|
||||
|
||||
[ -f "$OPERATOR_DIR/install.sh" ] || fail "install.sh not found after clone. Invalid repo."
|
||||
ok "install.sh found."
|
||||
|
||||
# ─── INSTALL ──────────────────────────────────────────────────────────────────
|
||||
sep
|
||||
info "Running installer..."
|
||||
info "WireGuard will NOT start — 02 will start it with the legacy identity."
|
||||
echo ""
|
||||
|
||||
cd "$OPERATOR_DIR"
|
||||
sudo systemctl start nginx 2>/dev/null || true
|
||||
sudo MIGRATION_MODE=1 bash install.sh || fail "Installer failed. Check output above."
|
||||
|
||||
if _env_is_corrupt "$OPERATOR_DIR/.env"; then
|
||||
fail "install.sh finished but generated an invalid .env. Check install.sh."
|
||||
fi
|
||||
ok ".env generated with valid values."
|
||||
|
||||
# ─── WG0.CONF PLACEHOLDER ─────────────────────────────────────────────────────
|
||||
sep
|
||||
info "Checking wg0.conf..."
|
||||
WG_CONF_DIR="$OPERATOR_DIR/services/wireguard/config/wg_confs"
|
||||
WG_CONF_FILE="$WG_CONF_DIR/wg0.conf"
|
||||
|
||||
if [ ! -f "$WG_CONF_FILE" ]; then
|
||||
warn "wg0.conf missing (MIGRATION_MODE skips WireGuard). Creating placeholder..."
|
||||
mkdir -p "$WG_CONF_DIR"
|
||||
cat > "$WG_CONF_FILE" << 'WGEOF'
|
||||
[Interface]
|
||||
Address = 10.13.13.1/16
|
||||
ListenPort = 51820
|
||||
PrivateKey = PLACEHOLDER
|
||||
PostUp = /config/scripts/post-up.sh
|
||||
PreDown = /config/scripts/pre-down.sh
|
||||
WGEOF
|
||||
ok "Placeholder created. 02 will inject the legacy private key."
|
||||
else
|
||||
ok "wg0.conf already exists."
|
||||
fi
|
||||
|
||||
# ─── RESTORE EXTERNAL NGINX CONFIGS ───────────────────────────────────────────
|
||||
sep
|
||||
info "Restoring external Nginx configs..."
|
||||
|
||||
for SITE in "$BACKUP_DIR/nginx/sites-available/"*; do
|
||||
NAME=$(basename "$SITE")
|
||||
if ! grep -q "$NAME" /etc/nginx/sites-available/* 2>/dev/null; then
|
||||
sudo cp "$SITE" "/etc/nginx/sites-available/$NAME" 2>/dev/null || true
|
||||
if [ -L "$BACKUP_DIR/nginx/sites-enabled/$NAME" ] || \
|
||||
[ -f "$BACKUP_DIR/nginx/sites-enabled/$NAME" ]; then
|
||||
sudo ln -sf "/etc/nginx/sites-available/$NAME" \
|
||||
"/etc/nginx/sites-enabled/$NAME" 2>/dev/null || true
|
||||
ok "Config restored: $NAME"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
sudo nginx -t && sudo systemctl reload nginx 2>/dev/null \
|
||||
|| warn "Nginx reload failed — check manually."
|
||||
|
||||
trap - ERR
|
||||
echo "[END-01] $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG"
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " Installation complete"
|
||||
echo " Next: bash 02-migrate-peers.sh"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
Loading…
Reference in a new issue