small fixes
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# --------------------------------- Failover Test ----------------------------------------------
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# Controlled simulation of the failover scenario — validates the entire failover lifecycle
|
||||
# without waiting for a real outage.
|
||||
#
|
||||
# This script is a TEST HARNESS only — it does not contain failover logic.
|
||||
# All failover logic lives in failover.sh and is called directly from here.
|
||||
# Any changes to failover.sh are automatically reflected in this test.
|
||||
#
|
||||
# Test sequence:
|
||||
# 1. Pre-flight — verify both servers reachable, failover.sh exists, state is NORMAL
|
||||
# 2. Block — add iptables rule dropping all traffic to remote IP
|
||||
# 3. Detect — run failover.sh one cycle — confirm FAILOVER state detected
|
||||
# 4. Start — verify failover containers started locally
|
||||
# 5. Restore — remove iptables rule, remote becomes reachable again
|
||||
# 6. Handback — wait for failover.sh to confirm handback strikes and hand back
|
||||
# 7. Verify — confirm containers returned to remote, local copies stopped
|
||||
# 8. Report — full pass/fail summary per phase
|
||||
#
|
||||
# Safety: iptables rule is removed via trap on ANY exit — crash, error, ctrl-c, or normal.
|
||||
# Remote connectivity is always restored regardless of test outcome.
|
||||
#
|
||||
# ⚠️ This script starts and stops real containers on both servers.
|
||||
# Run during a maintenance window — users will experience a brief service interruption.
|
||||
# Use --dry-run to walk through the sequence without touching containers or iptables.
|
||||
#
|
||||
# All configuration in Master.conf under Failover and Failover Test sections.
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$SCRIPT_DIR/../Master.conf"
|
||||
source "$SCRIPT_DIR/../common.sh"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
FAILOVER_SCRIPT="$SCRIPT_DIR/failover.sh"
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# SAFETY TRAP — always remove iptables rule on exit
|
||||
# Fires on normal exit, error exit, ctrl-c, and script crashes
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
IPTABLES_RULE_ACTIVE=false
|
||||
|
||||
cleanup() {
|
||||
if [[ "$IPTABLES_RULE_ACTIVE" == true ]]; then
|
||||
echo ""
|
||||
warn "$ICON_SHIELD Cleanup — removing iptables block on $REMOTE_SERVER..."
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
iptables -D OUTPUT -d "$REMOTE_SERVER" -j DROP 2>/dev/null
|
||||
IPTABLES_RULE_ACTIVE=false
|
||||
success "iptables rule removed — remote connectivity restored"
|
||||
else
|
||||
warn "DRY RUN — would remove iptables rule"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ $ICON_GEAR Setup ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Setup ━━━"
|
||||
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
error "Must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
success "Running as root"
|
||||
|
||||
if ! command -v iptables >/dev/null 2>&1; then
|
||||
error "iptables not found — required for connectivity simulation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
success "iptables available"
|
||||
|
||||
if [[ ! -f "$FAILOVER_SCRIPT" ]]; then
|
||||
error "failover.sh not found at $FAILOVER_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
success "failover.sh found"
|
||||
|
||||
detect_hosts
|
||||
resolve_remote_ip
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no iptables rules or container changes will be made"
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ $ICON_SUMMARY Status ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
if [[ "$SHOW_STATUS" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||||
echo "$ICON_HOST Local: $LOCAL_SERVER_NAME"
|
||||
echo "$ICON_HOST Remote: $REMOTE_SERVER_NAME ($REMOTE_SERVER)"
|
||||
echo "$ICON_FAILOVER Block wait: ${FAILOVER_TEST_BLOCK_WAIT}s"
|
||||
echo "$ICON_FAILOVER Handback wait: ${FAILOVER_TEST_HANDBACK_WAIT}s"
|
||||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||||
|
||||
# Current failover state
|
||||
if [[ -f "$FAILOVER_STATE_FILE" ]]; then
|
||||
CURRENT_STATE=$(grep "^state=" "$FAILOVER_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||||
echo "$ICON_FAILOVER Current state: ${CURRENT_STATE:-unknown}"
|
||||
else
|
||||
echo "$ICON_FAILOVER Current state: no state file"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# PHASE TRACKING
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
PHASES_PASS=()
|
||||
PHASES_FAIL=()
|
||||
TOTAL_START=$(date +%s)
|
||||
|
||||
phase_pass() { PHASES_PASS+=("$1"); success "$ICON_DONE Phase: $1 — PASSED"; }
|
||||
phase_fail() { PHASES_FAIL+=("$1"); error "$ICON_ERROR Phase: $1 — FAILED"; }
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ PHASE 1 — Pre-flight ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " $ICON_SHIELD FAILOVER TEST — $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo " $ICON_HOST Local: $LOCAL_SERVER_NAME"
|
||||
echo " $ICON_HOST Remote: $REMOTE_SERVER_NAME ($REMOTE_SERVER)"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "━━━ $ICON_SHIELD Phase 1 — Pre-flight ━━━"
|
||||
|
||||
# Check remote reachable
|
||||
info "Checking remote reachability..."
|
||||
if ping_remote; then
|
||||
success "Remote $REMOTE_SERVER_NAME is reachable"
|
||||
else
|
||||
error "Remote $REMOTE_SERVER_NAME is not reachable — cannot run test"
|
||||
phase_fail "Pre-flight"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check internet reachable
|
||||
info "Checking internet connectivity..."
|
||||
if ping_internet; then
|
||||
success "Internet is reachable"
|
||||
else
|
||||
error "No internet connectivity — cannot run test"
|
||||
phase_fail "Pre-flight"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check current failover state is NORMAL
|
||||
if [[ -f "$FAILOVER_STATE_FILE" ]]; then
|
||||
CURRENT_STATE=$(grep "^state=" "$FAILOVER_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||||
if [[ "$CURRENT_STATE" != "NORMAL" ]]; then
|
||||
error "Failover state is $CURRENT_STATE — must be NORMAL before running test"
|
||||
phase_fail "Pre-flight"
|
||||
exit 1
|
||||
fi
|
||||
success "Failover state is NORMAL"
|
||||
else
|
||||
warn "No state file found — assuming NORMAL (first run)"
|
||||
fi
|
||||
|
||||
phase_pass "Pre-flight"
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ PHASE 2 — Block Remote ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━ $ICON_PING Phase 2 — Block Remote Connectivity ━━━"
|
||||
warn "Adding iptables rule — dropping all traffic to $REMOTE_SERVER"
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
iptables -I OUTPUT -d "$REMOTE_SERVER" -j DROP
|
||||
IPTABLES_RULE_ACTIVE=true
|
||||
success "iptables rule active — $REMOTE_SERVER_NAME appears unreachable"
|
||||
|
||||
# Verify block is working
|
||||
if ! ping -c1 -W2 "$REMOTE_SERVER" &>/dev/null; then
|
||||
success "Connectivity block confirmed — ping to remote fails as expected"
|
||||
phase_pass "Block Remote"
|
||||
else
|
||||
error "iptables rule did not block connectivity — ping still succeeds"
|
||||
phase_fail "Block Remote"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would block $REMOTE_SERVER with iptables"
|
||||
phase_pass "Block Remote"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ PHASE 3 — Failover Detection ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━ $ICON_FAILOVER Phase 3 — Failover Detection ━━━"
|
||||
info "Waiting ${FAILOVER_TEST_BLOCK_WAIT}s for failover.sh to detect outage..."
|
||||
info "failover.sh check interval is ${FAILOVER_CHECK_INTERVAL}s"
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
sleep "$FAILOVER_TEST_BLOCK_WAIT"
|
||||
|
||||
# Check state file updated to FAILOVER
|
||||
if [[ -f "$FAILOVER_STATE_FILE" ]]; then
|
||||
NEW_STATE=$(grep "^state=" "$FAILOVER_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||||
if [[ "$NEW_STATE" == "FAILOVER" ]]; then
|
||||
success "State changed to FAILOVER — outage detected correctly"
|
||||
phase_pass "Failover Detection"
|
||||
else
|
||||
error "State is $NEW_STATE — expected FAILOVER after ${FAILOVER_TEST_BLOCK_WAIT}s"
|
||||
warn "failover.sh may not be running — check User Scripts plugin"
|
||||
phase_fail "Failover Detection"
|
||||
fi
|
||||
else
|
||||
error "No state file found after wait — failover.sh may not be running"
|
||||
phase_fail "Failover Detection"
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would wait ${FAILOVER_TEST_BLOCK_WAIT}s and check for FAILOVER state"
|
||||
phase_pass "Failover Detection"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ PHASE 4 — Container Start Verification ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Phase 4 — Failover Containers Started ━━━"
|
||||
|
||||
# Determine which containers should have started on this host
|
||||
if [[ "$LOCAL_SERVER_NAME" == "$HOST1" ]]; then
|
||||
EXPECTED_CONTAINERS=("${FAILOVER_HOST1_STARTS_FOR_HOST2[@]}")
|
||||
else
|
||||
EXPECTED_CONTAINERS=("${FAILOVER_HOST2_STARTS_FOR_HOST1[@]}")
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
CONTAINERS_OK=true
|
||||
for container in "${EXPECTED_CONTAINERS[@]}"; do
|
||||
[[ -z "$container" ]] && continue
|
||||
STATUS=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)
|
||||
if [[ "$STATUS" == "true" ]]; then
|
||||
success "$ICON_RUNNING $container is running locally"
|
||||
else
|
||||
error "$ICON_NOT_RUNNING $container is NOT running locally"
|
||||
CONTAINERS_OK=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$CONTAINERS_OK" == true ]]; then
|
||||
phase_pass "Container Start"
|
||||
else
|
||||
phase_fail "Container Start"
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would verify these containers started: ${EXPECTED_CONTAINERS[*]}"
|
||||
phase_pass "Container Start"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ PHASE 5 — Restore Connectivity ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━ $ICON_PING Phase 5 — Restore Remote Connectivity ━━━"
|
||||
info "Removing iptables block — remote becomes reachable again"
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
iptables -D OUTPUT -d "$REMOTE_SERVER" -j DROP 2>/dev/null
|
||||
IPTABLES_RULE_ACTIVE=false
|
||||
success "iptables rule removed"
|
||||
|
||||
# Verify connectivity restored
|
||||
sleep 3
|
||||
if ping_remote; then
|
||||
success "Remote $REMOTE_SERVER_NAME is reachable again"
|
||||
phase_pass "Restore Connectivity"
|
||||
else
|
||||
error "Remote still unreachable after removing iptables rule"
|
||||
phase_fail "Restore Connectivity"
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would remove iptables rule"
|
||||
phase_pass "Restore Connectivity"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ PHASE 6 — Handback ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━ $ICON_FAILOVER Phase 6 — Handback ━━━"
|
||||
info "Waiting ${FAILOVER_TEST_HANDBACK_WAIT}s for failover.sh to confirm handback..."
|
||||
info "Requires $FAILOVER_HANDBACK_STRIKES consecutive remote-up checks at ${FAILOVER_CHECK_INTERVAL}s intervals"
|
||||
info "Estimated minimum wait: $(( FAILOVER_HANDBACK_STRIKES * FAILOVER_CHECK_INTERVAL ))s"
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
sleep "$FAILOVER_TEST_HANDBACK_WAIT"
|
||||
|
||||
if [[ -f "$FAILOVER_STATE_FILE" ]]; then
|
||||
FINAL_STATE=$(grep "^state=" "$FAILOVER_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||||
if [[ "$FINAL_STATE" == "NORMAL" ]]; then
|
||||
success "State returned to NORMAL — handback completed"
|
||||
phase_pass "Handback"
|
||||
else
|
||||
error "State is $FINAL_STATE — expected NORMAL after handback wait"
|
||||
phase_fail "Handback"
|
||||
fi
|
||||
else
|
||||
error "No state file found"
|
||||
phase_fail "Handback"
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would wait ${FAILOVER_TEST_HANDBACK_WAIT}s and verify NORMAL state"
|
||||
phase_pass "Handback"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ PHASE 7 — Container Handback Verification ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Phase 7 — Failover Containers Stopped Locally ━━━"
|
||||
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
HANDBACK_OK=true
|
||||
for container in "${EXPECTED_CONTAINERS[@]}"; do
|
||||
[[ -z "$container" ]] && continue
|
||||
STATUS=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)
|
||||
if [[ "$STATUS" != "true" ]]; then
|
||||
success "$ICON_NOT_RUNNING $container stopped locally — handed back"
|
||||
else
|
||||
error "$ICON_RUNNING $container still running locally — handback may have failed"
|
||||
HANDBACK_OK=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$HANDBACK_OK" == true ]]; then
|
||||
phase_pass "Container Handback"
|
||||
else
|
||||
phase_fail "Container Handback"
|
||||
fi
|
||||
else
|
||||
warn "DRY RUN — would verify failover containers stopped locally after handback"
|
||||
phase_pass "Container Handback"
|
||||
fi
|
||||
|
||||
TOTAL_END=$(date +%s)
|
||||
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
# ━━━ $ICON_SUMMARY Test Report ━━━
|
||||
# -----------------------------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY FAILOVER TEST REPORT ━━━━━"
|
||||
echo "$ICON_HOST Local: $LOCAL_SERVER_NAME"
|
||||
echo "$ICON_HOST Remote: $REMOTE_SERVER_NAME"
|
||||
echo "$ICON_TIME Duration: $(format_duration $((TOTAL_END - TOTAL_START)))"
|
||||
echo ""
|
||||
echo " Phase Results:"
|
||||
for phase in "${PHASES_PASS[@]}"; do
|
||||
echo " $ICON_SUCCESS $phase"
|
||||
done
|
||||
for phase in "${PHASES_FAIL[@]}"; do
|
||||
echo " $ICON_ERROR $phase"
|
||||
done
|
||||
echo ""
|
||||
|
||||
PASS_COUNT=${#PHASES_PASS[@]}
|
||||
FAIL_COUNT=${#PHASES_FAIL[@]}
|
||||
TOTAL_PHASES=$(( PASS_COUNT + FAIL_COUNT ))
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
||||
elif [[ "$FAIL_COUNT" -eq 0 ]]; then
|
||||
echo "$ICON_DONE Status: $ICON_SUCCESS ALL $TOTAL_PHASES PHASES PASSED"
|
||||
notify "Failover test PASSED on $(hostname) — all $TOTAL_PHASES phases completed successfully" "Failover Test" "normal"
|
||||
else
|
||||
echo "$ICON_ERROR Status: $FAIL_COUNT/$TOTAL_PHASES PHASES FAILED"
|
||||
notify "Failover test FAILED on $(hostname) — $FAIL_COUNT/$TOTAL_PHASES phases failed: ${PHASES_FAIL[*]}" "Failover Test" "warning"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
[[ "$FAIL_COUNT" -gt 0 ]] && exit 1
|
||||
exit 0
|
||||
Reference in New Issue
Block a user