#!/bin/bash # ============================================================================== # Wordly.art - NPM Failover via API # ============================================================================== # Automatically updates Nginx Proxy Manager's forward host via its REST API. # Called by disaster-recovery.sh after a successful health check on the new server. # # Usage: # ./npm-failover.sh --target-ip 192.168.1.98 # Switch to new server # ./npm-failover.sh --target-ip 192.168.1.151 # Rollback to original server # ./npm-failover.sh --dry-run --target-ip 192.168.1.98 # Test without modifying NPM # ============================================================================== set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" TIMESTAMP=$(date +"%Y%m%d_%H%M%S") RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log() { echo -e "[NPM-Failover ${TIMESTAMP}] $1"; } log_success() { echo -e "[NPM-Failover ${TIMESTAMP}] ${GREEN}$1${NC}"; } log_warning() { echo -e "[NPM-Failover ${TIMESTAMP}] ${YELLOW}WARNING: $1${NC}"; } log_error() { echo -e "[NPM-Failover ${TIMESTAMP}] ${RED}ERROR: $1${NC}"; } log_info() { echo -e "[NPM-Failover ${TIMESTAMP}] ${BLUE}$1${NC}"; } # ============================================================================== # 1. LOAD CONFIGURATION FROM .env # ============================================================================== ENV_FILE="${PROJECT_ROOT}/.env" if [ -f "${ENV_FILE}" ]; then set -a set +u source "${ENV_FILE}" set -u set +a fi NPM_API_URL="${NPM_API_URL:-}" NPM_ADMIN_EMAIL="${NPM_ADMIN_EMAIL:-}" NPM_ADMIN_PASSWORD="${NPM_ADMIN_PASSWORD:-}" NPM_PROXY_HOST_DOMAIN="${NPM_PROXY_HOST_DOMAIN:-wordly.art}" TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-}" # ============================================================================== # 2. ARGUMENT PARSING # ============================================================================== TARGET_IP="" DRY_RUN=false while [[ $# -gt 0 ]]; do case "$1" in --target-ip) TARGET_IP="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; *) log_error "Unknown argument: $1" echo "Usage: $0 --target-ip [--dry-run]" exit 1 ;; esac done # ============================================================================== # 3. VALIDATION # ============================================================================== validate_config() { local errors=0 if [ -z "${TARGET_IP}" ]; then log_error "--target-ip is required." errors=$((errors + 1)) fi if [ -z "${NPM_API_URL}" ]; then log_error "NPM_API_URL is not set in .env (example: http://192.168.1.184:81/api)" errors=$((errors + 1)) fi if [ -z "${NPM_ADMIN_EMAIL}" ]; then log_error "NPM_ADMIN_EMAIL is not set in .env" errors=$((errors + 1)) fi if [ -z "${NPM_ADMIN_PASSWORD}" ]; then log_error "NPM_ADMIN_PASSWORD is not set in .env" errors=$((errors + 1)) fi if ! command -v curl &>/dev/null; then log_error "curl is not installed. Required for NPM API calls." errors=$((errors + 1)) fi if ! command -v jq &>/dev/null; then log_error "jq is not installed. Required for JSON parsing. Install: apt-get install jq" errors=$((errors + 1)) fi if [ "${errors}" -gt 0 ]; then exit 1 fi } # ============================================================================== # 4. TELEGRAM NOTIFICATION # ============================================================================== send_telegram() { local message="$1" if [ -n "${TELEGRAM_BOT_TOKEN}" ] && [ -n "${TELEGRAM_CHAT_ID}" ]; then curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${TELEGRAM_CHAT_ID}" \ -d "text=${message}" \ -d "parse_mode=Markdown" \ >/dev/null 2>&1 || true fi } # ============================================================================== # 5. NPM API AUTHENTICATION # ============================================================================== npm_authenticate() { log "Authenticating with NPM API at ${NPM_API_URL}..." local response response=$(curl -s -w "\n%{http_code}" \ -X POST "${NPM_API_URL}/tokens" \ -H "Content-Type: application/json" \ -d "{\"identity\": \"${NPM_ADMIN_EMAIL}\", \"secret\": \"${NPM_ADMIN_PASSWORD}\"}" \ --connect-timeout 10 \ --max-time 15) local http_code http_code=$(echo "${response}" | tail -n1) local body body=$(echo "${response}" | head -n-1) if [ "${http_code}" != "200" ]; then log_error "NPM authentication failed (HTTP ${http_code}). Check NPM_ADMIN_EMAIL and NPM_ADMIN_PASSWORD." log_error "Response: ${body}" return 1 fi local token token=$(echo "${body}" | jq -r '.token // empty') if [ -z "${token}" ]; then log_error "Could not extract token from NPM response." log_error "Response: ${body}" return 1 fi log_success "NPM authentication successful." echo "${token}" } # ============================================================================== # 6. FIND PROXY HOST BY DOMAIN # ============================================================================== npm_find_proxy_host() { local token="$1" log "Looking up proxy host for domain: ${NPM_PROXY_HOST_DOMAIN}..." local response response=$(curl -s -w "\n%{http_code}" \ -X GET "${NPM_API_URL}/nginx/proxy-hosts?expand=domain_names" \ -H "Authorization: Bearer ${token}" \ --connect-timeout 10 \ --max-time 15) local http_code http_code=$(echo "${response}" | tail -n1) local body body=$(echo "${response}" | head -n-1) if [ "${http_code}" != "200" ]; then log_error "Failed to retrieve proxy hosts (HTTP ${http_code})" return 1 fi # Find the proxy host ID matching our domain local host_id host_id=$(echo "${body}" | jq -r \ --arg domain "${NPM_PROXY_HOST_DOMAIN}" \ '.[] | select(.domain_names[] == $domain) | .id' | head -n1) if [ -z "${host_id}" ]; then log_error "No proxy host found for domain '${NPM_PROXY_HOST_DOMAIN}' in NPM." log_error "Available domains:" echo "${body}" | jq -r '.[].domain_names[]' | sed 's/^/ - /' >&2 return 1 fi log_success "Found proxy host ID: ${host_id} for ${NPM_PROXY_HOST_DOMAIN}" # Also retrieve current forward_host for logging local current_host current_host=$(echo "${body}" | jq -r \ --arg domain "${NPM_PROXY_HOST_DOMAIN}" \ '.[] | select(.domain_names[] == $domain) | .forward_host' | head -n1) log_info "Current forward host: ${current_host}" echo "${host_id}|${current_host}" } # ============================================================================== # 7. UPDATE PROXY HOST FORWARD IP # ============================================================================== npm_update_proxy_host() { local token="$1" local host_id="$2" local new_ip="$3" log "Updating proxy host ${host_id} → forward to ${new_ip}..." # First, get the full current configuration to preserve all existing settings local current_config current_config=$(curl -s \ -X GET "${NPM_API_URL}/nginx/proxy-hosts/${host_id}" \ -H "Authorization: Bearer ${token}" \ --connect-timeout 10 \ --max-time 15) # Build the update payload preserving existing config, only changing forward_host local update_payload update_payload=$(echo "${current_config}" | jq \ --arg new_ip "${new_ip}" \ '. + {"forward_host": $new_ip}') if [ "${DRY_RUN}" = "true" ]; then log_warning "[DRY RUN] Would send PUT to ${NPM_API_URL}/nginx/proxy-hosts/${host_id}" log_warning "[DRY RUN] Payload: ${update_payload}" log_success "[DRY RUN] NPM failover simulation complete — no changes made." return 0 fi local response response=$(curl -s -w "\n%{http_code}" \ -X PUT "${NPM_API_URL}/nginx/proxy-hosts/${host_id}" \ -H "Authorization: Bearer ${token}" \ -H "Content-Type: application/json" \ -d "${update_payload}" \ --connect-timeout 10 \ --max-time 15) local http_code http_code=$(echo "${response}" | tail -n1) local body body=$(echo "${response}" | head -n-1) if [ "${http_code}" != "200" ]; then log_error "Failed to update proxy host (HTTP ${http_code})" log_error "Response: ${body}" return 1 fi # Verify the change was applied local confirmed_host confirmed_host=$(echo "${body}" | jq -r '.forward_host // empty') if [ "${confirmed_host}" != "${new_ip}" ]; then log_error "NPM accepted the request but the forward_host is '${confirmed_host}', expected '${new_ip}'." return 1 fi log_success "NPM proxy host updated successfully: ${NPM_PROXY_HOST_DOMAIN} → ${new_ip}" } # ============================================================================== # 8. MAIN # ============================================================================== main() { echo "" echo "=========================================================" echo " Wordly.art — NPM Failover" echo " Target IP : ${TARGET_IP:-NOT SET}" echo " NPM API : ${NPM_API_URL:-NOT SET}" echo " Domain : ${NPM_PROXY_HOST_DOMAIN}" echo " Dry Run : ${DRY_RUN}" echo "=========================================================" echo "" validate_config # Step 1: Authenticate local token token=$(npm_authenticate) # Step 2: Find proxy host ID and current IP local host_info host_info=$(npm_find_proxy_host "${token}") local host_id="${host_info%%|*}" local current_ip="${host_info##*|}" if [ "${current_ip}" = "${TARGET_IP}" ]; then log_warning "NPM already points to ${TARGET_IP}. No change needed." exit 0 fi # Step 3: Update forward host npm_update_proxy_host "${token}" "${host_id}" "${TARGET_IP}" # Step 4: Notify if [ "${DRY_RUN}" = "false" ]; then local msg="🔀 *Wordly.art NPM Failover* Domaine : \`${NPM_PROXY_HOST_DOMAIN}\` Ancien serveur : \`${current_ip}\` Nouveau serveur : \`${TARGET_IP}\` Heure : $(date '+%Y-%m-%d %H:%M:%S')" send_telegram "${msg}" log_success "Telegram notification sent." fi echo "" log_success "=========================================================" log_success "NPM Failover COMPLETE" log_success " ${NPM_PROXY_HOST_DOMAIN} now routes to → ${TARGET_IP}" log_success "=========================================================" echo "" } main "$@"