Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
326 lines
11 KiB
Bash
326 lines
11 KiB
Bash
#!/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
|
|
source "${ENV_FILE}"
|
|
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 <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 "$@"
|