feat: add NAS backup, verification, and DR scripts
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
This commit is contained in:
325
scripts/npm-failover.sh
Normal file
325
scripts/npm-failover.sh
Normal file
@@ -0,0 +1,325 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user