Chunker splits on the header sections the audit standardised, then sub-splits named-paragraph safeguards — without that a specific question about one of rsync.sh's fourteen safeguards scored below unrelated chunks, because the other thirteen dominated the vector. Index is SQLite with raw float32 blobs and is incremental on mtime; a no-op re-index takes 66ms. The stale defaults were found by asking the system a question and checking its answer: it correctly reported what mover_stop.sh's header claimed, and the header was wrong.
550 lines
23 KiB
Bash
Executable File
550 lines
23 KiB
Bash
Executable File
#!/bin/bash
|
||
# ==============================================================================================
|
||
# ================================= Fallback Test ==============================================
|
||
# ==============================================================================================
|
||
#
|
||
# PURPOSE
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Controlled simulation of the fallback lifecycle. Validates the entire sequence
|
||
# without waiting for a real outage. Contains no fallback logic — exercises the
|
||
# real fallback.sh via an iptables DROP rule on the remote Tailscale IP.
|
||
#
|
||
# Run during a maintenance window. Users will experience a brief service
|
||
# interruption. Use --dry-run to walk through all phases without real changes.
|
||
#
|
||
# ==============================================================================================
|
||
# OPERATIONAL MODEL
|
||
# ==============================================================================================
|
||
#
|
||
# Phase 1 — Pre-flight Both servers reachable, Docker daemons healthy,
|
||
# version parity, fallback.sh exists, state NORMAL
|
||
# Phase 2 — Block Remote iptables DROP rule added — remote appears unreachable
|
||
# Phase 3 — Fallback Detection Wait FALLBACK_TEST_BLOCK_WAIT for fallback.sh to
|
||
# detect the outage and enter FALLBACK state
|
||
# Phase 4 — Container Start Verify Tier 1 containers started locally
|
||
# Phase 5 — Restore iptables rule removed — remote reachable again
|
||
# Phase 6 — Handback Wait FALLBACK_TEST_HANDBACK_WAIT for fallback.sh to
|
||
# complete full handback and return to NORMAL
|
||
# Phase 7 — Container Handback Verify Tier 1 containers stopped locally
|
||
# Report — Full pass/fail per phase with timing
|
||
#
|
||
# ==============================================================================================
|
||
# DESIGN PRINCIPLES
|
||
# ==============================================================================================
|
||
#
|
||
# Test Harness Only
|
||
# Contains zero fallback logic. All fallback is exercised through fallback.sh.
|
||
# Any change to fallback.sh is automatically reflected in the test result.
|
||
#
|
||
# ==============================================================================================
|
||
# OPERATIONAL SAFEGUARDS
|
||
# ==============================================================================================
|
||
#
|
||
# iptables Safety Trap
|
||
# The DROP rule is removed via an EXIT trap that fires on normal completion, error
|
||
# exit, script crash, ctrl-c (SIGINT) and SIGTERM — verified, not assumed. Remote
|
||
# connectivity is restored regardless of test outcome.
|
||
#
|
||
# Stale Rule Sweep
|
||
# The trap above cannot cover SIGKILL or a power cut, which are the only ways a DROP
|
||
# rule survives the test. One stranded that way makes fallback.sh see the partner as
|
||
# permanently down and hold FALLBACK indefinitely, so pre-flight clears any leftover
|
||
# rule before doing anything else — including before the reachability check, which
|
||
# would otherwise fail and blame the network for the test's own residue.
|
||
#
|
||
# Root Enforcement
|
||
# iptables and container control require root.
|
||
#
|
||
# iptables Presence Check
|
||
# platform_require_cmd confirms iptables exists before the test begins — there is no
|
||
# point entering a connectivity simulation that cannot simulate anything.
|
||
#
|
||
# FALLBACK_ENABLED Gate
|
||
# Aborts if FALLBACK_ENABLED=false. Testing a disabled fallback system is
|
||
# misleading and potentially destructive.
|
||
#
|
||
# State Must Be NORMAL
|
||
# Pre-flight fails if state is not NORMAL. Running a test during an actual
|
||
# fallback event would interfere with the real event.
|
||
#
|
||
# Version Parity Check
|
||
# Pre-flight verifies unRAID version parity before any iptables rules are
|
||
# added. A mismatch makes the test result unreliable.
|
||
#
|
||
# Remote Docker Daemon Check
|
||
# Pre-flight confirms remote Docker daemon is responsive before Phase 2.
|
||
#
|
||
# Lock Acquisition
|
||
# acquire_lock() prevents concurrent test runs. Running two tests simultaneously
|
||
# would produce conflicting iptables rules and unreliable results.
|
||
#
|
||
# Host Detection
|
||
# detect_hosts() resolves MY_ID / REMOTE_ID from master.conf at startup.
|
||
# Exits if the host cannot be identified — prevents testing on an unknown machine.
|
||
#
|
||
# ==============================================================================================
|
||
# CONFIGURATION
|
||
# ==============================================================================================
|
||
#
|
||
# master.conf
|
||
#
|
||
# FALLBACK_TEST_BLOCK_WAIT
|
||
# Seconds to wait in Phase 3 for fallback.sh to detect the outage.
|
||
# Must be > FALLBACK_CHECK_INTERVAL + buffer. At 30s interval: use ≥60s.
|
||
# (shipped default: 150)
|
||
#
|
||
# FALLBACK_TEST_HANDBACK_WAIT
|
||
# Seconds to wait in Phase 6 for fallback.sh to complete handback.
|
||
# Must cover: FALLBACK_HANDBACK_STRIKES × FALLBACK_CHECK_INTERVAL + rsync
|
||
# duration + container start time. At 3 strikes × 30s + ~2min rsync +
|
||
# ~1min container start: use ≥240s. (shipped default: 360)
|
||
#
|
||
# ==============================================================================================
|
||
# RUNTIME MODES
|
||
# ==============================================================================================
|
||
#
|
||
# fallback_test.sh --dry-run
|
||
# Walk through all 7 phases with output but no iptables changes and no
|
||
# container starts/stops. ALWAYS run this before a live test.
|
||
#
|
||
# fallback_test.sh
|
||
# Full live test — real iptables DROP rule, real container lifecycle.
|
||
# Users will experience a brief service interruption. Run during a
|
||
# maintenance window.
|
||
#
|
||
# fallback_test.sh --status
|
||
# Show current fallback state and test timing configuration. No test run.
|
||
#
|
||
# fallback_test.sh --log
|
||
# Verbose output on every check in every phase.
|
||
#
|
||
# ==============================================================================================
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
||
source "$SCRIPT_DIR/../load_config.sh"
|
||
|
||
parse_args "$@"
|
||
|
||
FALLBACK_SCRIPT="$SCRIPT_DIR/fallback.sh"
|
||
DOCKER_TIMEOUT=15
|
||
|
||
# ==============================================================================================
|
||
# ── SAFETY TRAP — always remove iptables rule on exit ─────────────────────────────────────────
|
||
# ==============================================================================================
|
||
# Fires on normal exit, error exit, ctrl-c, and script crashes.
|
||
# Remote connectivity is ALWAYS restored regardless of test outcome.
|
||
|
||
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
|
||
warn "iptables rule removed — remote connectivity restored"
|
||
else
|
||
warn "DRY RUN — would remove iptables rule"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
trap cleanup EXIT
|
||
|
||
# ── Stale rule sweep — the one case the trap above cannot cover ───────────────────────────────
|
||
# The EXIT trap fires on normal exit, error, ctrl-c and SIGTERM, but not on SIGKILL or a power
|
||
# cut. A DROP rule stranded that way makes fallback.sh see the partner as permanently down and
|
||
# sit in FALLBACK indefinitely — so clear any leftover from a previous run before starting.
|
||
_clear_stale_block() {
|
||
[[ -z "${REMOTE_SERVER:-}" ]] && return
|
||
local removed=0
|
||
while iptables -C OUTPUT -d "$REMOTE_SERVER" -j DROP 2>/dev/null; do
|
||
if [[ "$DRY_RUN" == true ]]; then
|
||
warn "DRY RUN — would remove stale iptables block on $REMOTE_SERVER"
|
||
return
|
||
fi
|
||
iptables -D OUTPUT -d "$REMOTE_SERVER" -j DROP 2>/dev/null || break
|
||
(( removed++ ))
|
||
done
|
||
if [[ "$removed" -gt 0 ]]; then
|
||
warn "$ICON_SHIELD Removed $removed stale iptables block(s) on $REMOTE_SERVER from a previous run"
|
||
notify "Fallback test cleared $removed stale iptables block(s) on $(hostname) — a previous test was killed before cleanup" \
|
||
"Fallback Test" "warning"
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Setup ━━━
|
||
# ==============================================================================================
|
||
|
||
if [[ "$EUID" -ne 0 ]]; then
|
||
error "Must be run as root"
|
||
exit 1
|
||
fi
|
||
|
||
# FALLBACK_ENABLED gate — no point testing if fallback is disabled
|
||
# Fail-closed, matching fallback.sh — anything not exactly "true" counts as disabled.
|
||
if [[ "${FALLBACK_ENABLED:-false}" != "true" ]]; then
|
||
warn "FALLBACK_ENABLED=false — fallback test aborted"
|
||
warn "Enable fallback in master.conf before running this test"
|
||
exit 0
|
||
fi
|
||
|
||
acquire_lock # strict single instance — modifies iptables and containers
|
||
|
||
if ! command -v docker &>/dev/null; then
|
||
error "Docker command not found"
|
||
exit 1
|
||
fi
|
||
|
||
detect_hosts
|
||
resolve_remote_ip
|
||
|
||
# Validate commands used by this script
|
||
platform_require_cmd \
|
||
"$(which iptables 2>/dev/null || echo /sbin/iptables)" \
|
||
"--version" "iptables" \
|
||
"iptables" || { error "iptables not found — required for connectivity simulation"; exit 1; }
|
||
|
||
|
||
if [[ ! -f "$FALLBACK_SCRIPT" ]]; then
|
||
error "fallback.sh not found at $FALLBACK_SCRIPT"
|
||
exit 1
|
||
fi
|
||
log "fallback.sh found at $FALLBACK_SCRIPT"
|
||
log "$ICON_GEAR Config: remote=${REMOTE_SERVER_NAME} (${REMOTE_SERVER}) fallback-script=${FALLBACK_SCRIPT}"
|
||
|
||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no iptables rules or container changes will be made"
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Status ━━━
|
||
# ==============================================================================================
|
||
if [[ "$SHOW_STATUS" == true ]]; then
|
||
local_ver=$(platform_get_os_version 2>/dev/null || echo "unknown")
|
||
|
||
echo ""
|
||
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
|
||
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||
echo "$ICON_HOST Remote ID: $REMOTE_ID ($REMOTE_SERVER_NAME — $REMOTE_SERVER)"
|
||
echo "$ICON_GEAR OS ver: $local_ver"
|
||
echo "$ICON_FALLBACK Block wait: ${FALLBACK_TEST_BLOCK_WAIT}s"
|
||
echo "$ICON_FALLBACK Handback wait: ${FALLBACK_TEST_HANDBACK_WAIT}s"
|
||
echo "$ICON_FALLBACK Check interval: ${FALLBACK_CHECK_INTERVAL}s"
|
||
echo "$ICON_FALLBACK Handback strikes: ${FALLBACK_HANDBACK_STRIKES}"
|
||
echo "$ICON_GEAR Dry Run: $DRY_RUN"
|
||
|
||
if [[ -f "$FALLBACK_STATE_FILE" ]]; then
|
||
CURRENT_STATE=$(grep "^state=" "$FALLBACK_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||
echo "$ICON_FALLBACK Current state: ${CURRENT_STATE:-unknown}"
|
||
else
|
||
echo "$ICON_FALLBACK Current state: no state file"
|
||
fi
|
||
|
||
# Show Tier 1 containers for this host
|
||
TIER1_VAR="FALLBACK_${REMOTE_ID}_TIER1"
|
||
eval "TIER1_CONTAINERS=(\"\${${TIER1_VAR}[@]:-}\")"
|
||
echo "$ICON_CONTAINERS Tier 1 to test: ${TIER1_CONTAINERS[*]:-none configured}"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||
exit 0
|
||
fi
|
||
|
||
# ==============================================================================================
|
||
# ── PHASE TRACKING ────────────────────────────────────────────────────────────────────────────
|
||
# ==============================================================================================
|
||
PHASES_PASS=()
|
||
PHASES_FAIL=()
|
||
TOTAL_START=$(date +%s)
|
||
|
||
phase_pass() { PHASES_PASS+=("$1"); warn "$ICON_DONE Phase: $1 — PASSED ✅"; }
|
||
phase_fail() { PHASES_FAIL+=("$1"); error "Phase: $1 — FAILED ❌"; }
|
||
|
||
# Get Tier 1 containers defined in remote's own conf
|
||
TIER1_VAR="FALLBACK_${REMOTE_ID}_TIER1"
|
||
eval "TIER1_CONTAINERS=(\"\${${TIER1_VAR}[@]:-}\")"
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Phase 1 — Pre-flight ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " $ICON_SHIELD FALLBACK TEST — $(date '+%Y-%m-%d %H:%M:%S')"
|
||
echo " $ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) → $REMOTE_ID ($REMOTE_SERVER_NAME)"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
echo "━━━ $ICON_SHIELD Phase 1 — Pre-flight ━━━"
|
||
|
||
# Must run before the reachability check below — a stale DROP rule from a killed run makes
|
||
# the partner look unreachable, and the test would abort blaming the network for its own
|
||
# leftover.
|
||
_clear_stale_block
|
||
|
||
# Remote reachable
|
||
if ping_remote; then
|
||
log "$REMOTE_SERVER_NAME is reachable"
|
||
else
|
||
error "$REMOTE_SERVER_NAME is not reachable — cannot run test"
|
||
phase_fail "Pre-flight"
|
||
exit 1
|
||
fi
|
||
|
||
# Internet reachable
|
||
if ping_internet; then
|
||
log "Internet is reachable"
|
||
else
|
||
error "No internet connectivity — cannot run test"
|
||
phase_fail "Pre-flight"
|
||
exit 1
|
||
fi
|
||
|
||
# Version parity — test may produce misleading results on mismatch
|
||
if ! check_os_version_parity; then
|
||
error "unRAID version mismatch — test aborted to prevent misleading results"
|
||
phase_fail "Pre-flight"
|
||
exit 1
|
||
fi
|
||
|
||
# Remote Docker daemon — must be responsive before test manipulates containers
|
||
if ! check_remote_docker_daemon; then
|
||
error "Remote Docker daemon not responsive — cannot run test"
|
||
phase_fail "Pre-flight"
|
||
exit 1
|
||
fi
|
||
|
||
# Fallback state must be NORMAL before test
|
||
if [[ -f "$FALLBACK_STATE_FILE" ]]; then
|
||
CURRENT_STATE=$(grep "^state=" "$FALLBACK_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||
if [[ "$CURRENT_STATE" != "NORMAL" ]]; then
|
||
error "Fallback state is $CURRENT_STATE — must be NORMAL before running test"
|
||
phase_fail "Pre-flight"
|
||
exit 1
|
||
fi
|
||
log "Fallback state is NORMAL"
|
||
else
|
||
warn "No state file found — assuming NORMAL (first run)"
|
||
fi
|
||
|
||
# Tier 1 containers configured
|
||
if [[ ${#TIER1_CONTAINERS[@]} -eq 0 ]]; then
|
||
error "No Tier 1 containers configured for $MY_ID → $REMOTE_ID"
|
||
error "Check FALLBACK_${REMOTE_ID}_TIER1 in ${REMOTE_ID,,}.conf"
|
||
phase_fail "Pre-flight"
|
||
exit 1
|
||
fi
|
||
log "Tier 1 containers: ${TIER1_CONTAINERS[*]}"
|
||
|
||
phase_pass "Pre-flight"
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Phase 2 — Block Remote Connectivity ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━ $ICON_PING Phase 2 — Block Remote Connectivity ━━━"
|
||
warn "Adding iptables rule — dropping all traffic to $REMOTE_SERVER ($REMOTE_SERVER_NAME)"
|
||
|
||
if [[ "$DRY_RUN" == false ]]; then
|
||
iptables -I OUTPUT -d "$REMOTE_SERVER" -j DROP
|
||
IPTABLES_RULE_ACTIVE=true
|
||
|
||
# Verify block is working
|
||
sleep 2
|
||
if ! ping -c1 -W2 "$REMOTE_SERVER" &>/dev/null; then
|
||
log "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 DROP rule"
|
||
phase_pass "Block Remote"
|
||
fi
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Phase 3 — Fallback Detection ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━ $ICON_FALLBACK Phase 3 — Fallback Detection ━━━"
|
||
warn "Waiting ${FALLBACK_TEST_BLOCK_WAIT}s for fallback.sh to detect outage..."
|
||
log "fallback.sh check interval: ${FALLBACK_CHECK_INTERVAL}s"
|
||
|
||
if [[ "$DRY_RUN" == false ]]; then
|
||
sleep "$FALLBACK_TEST_BLOCK_WAIT"
|
||
|
||
if [[ -f "$FALLBACK_STATE_FILE" ]]; then
|
||
NEW_STATE=$(grep "^state=" "$FALLBACK_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||
if [[ "$NEW_STATE" == "FALLBACK" ]]; then
|
||
echo "State changed to FALLBACK — outage detected correctly ✅"
|
||
phase_pass "Fallback Detection"
|
||
else
|
||
error "State is $NEW_STATE — expected FALLBACK after ${FALLBACK_TEST_BLOCK_WAIT}s"
|
||
warn "Is fallback.sh running? Check User Scripts plugin"
|
||
phase_fail "Fallback Detection"
|
||
fi
|
||
else
|
||
error "No state file found after wait — fallback.sh may not be running"
|
||
phase_fail "Fallback Detection"
|
||
fi
|
||
else
|
||
warn "DRY RUN — would wait ${FALLBACK_TEST_BLOCK_WAIT}s then check for FALLBACK state"
|
||
phase_pass "Fallback Detection"
|
||
fi
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Phase 4 — Container Start Verification ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━ $ICON_CONTAINERS Phase 4 — Tier 1 Containers Started Locally ━━━"
|
||
log "Checking Tier 1 containers: ${TIER1_CONTAINERS[*]}"
|
||
|
||
if [[ "$DRY_RUN" == false ]]; then
|
||
CONTAINERS_OK=true
|
||
for container in "${TIER1_CONTAINERS[@]}"; do
|
||
[[ -z "$container" ]] && continue
|
||
STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
|
||
"$container" 2>/dev/null)
|
||
if [[ "$STATUS" == "true" ]]; then
|
||
echo "$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 Tier 1 containers started: ${TIER1_CONTAINERS[*]}"
|
||
phase_pass "Container Start"
|
||
fi
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Phase 5 — Restore Remote Connectivity ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━ $ICON_PING Phase 5 — Restore Remote Connectivity ━━━"
|
||
warn "Removing iptables block — $REMOTE_SERVER_NAME becomes reachable again"
|
||
|
||
if [[ "$DRY_RUN" == false ]]; then
|
||
iptables -D OUTPUT -d "$REMOTE_SERVER" -j DROP 2>/dev/null
|
||
IPTABLES_RULE_ACTIVE=false
|
||
|
||
sleep 3
|
||
if ping_remote; then
|
||
echo "$REMOTE_SERVER_NAME is reachable again ✅"
|
||
phase_pass "Restore Connectivity"
|
||
else
|
||
error "$REMOTE_SERVER_NAME 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_FALLBACK Phase 6 — Handback ━━━"
|
||
warn "Waiting ${FALLBACK_TEST_HANDBACK_WAIT}s for fallback.sh to complete handback..."
|
||
log "Requires $FALLBACK_HANDBACK_STRIKES consecutive checks at ${FALLBACK_CHECK_INTERVAL}s"
|
||
log "Minimum handback time: $(( FALLBACK_HANDBACK_STRIKES * FALLBACK_CHECK_INTERVAL ))s"
|
||
|
||
if [[ "$DRY_RUN" == false ]]; then
|
||
sleep "$FALLBACK_TEST_HANDBACK_WAIT"
|
||
|
||
if [[ -f "$FALLBACK_STATE_FILE" ]]; then
|
||
FINAL_STATE=$(grep "^state=" "$FALLBACK_STATE_FILE" 2>/dev/null | cut -d= -f2)
|
||
if [[ "$FINAL_STATE" == "NORMAL" ]]; then
|
||
echo "State returned to NORMAL — handback completed ✅"
|
||
phase_pass "Handback"
|
||
else
|
||
error "State is $FINAL_STATE — expected NORMAL after ${FALLBACK_TEST_HANDBACK_WAIT}s"
|
||
warn "Handback may still be in progress — check fallback.sh output"
|
||
phase_fail "Handback"
|
||
fi
|
||
else
|
||
error "No state file found"
|
||
phase_fail "Handback"
|
||
fi
|
||
else
|
||
warn "DRY RUN — would wait ${FALLBACK_TEST_HANDBACK_WAIT}s then verify NORMAL state"
|
||
phase_pass "Handback"
|
||
fi
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Phase 7 — Container Handback Verification ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━ $ICON_CONTAINERS Phase 7 — Tier 1 Containers Stopped Locally ━━━"
|
||
log "Verifying Tier 1 containers returned to $REMOTE_SERVER_NAME"
|
||
|
||
if [[ "$DRY_RUN" == false ]]; then
|
||
HANDBACK_OK=true
|
||
for container in "${TIER1_CONTAINERS[@]}"; do
|
||
[[ -z "$container" ]] && continue
|
||
STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
|
||
"$container" 2>/dev/null)
|
||
if [[ "$STATUS" != "true" ]]; then
|
||
echo "$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 Tier 1 containers stopped locally after handback"
|
||
phase_pass "Container Handback"
|
||
fi
|
||
|
||
TOTAL_END=$(date +%s)
|
||
|
||
# ==============================================================================================
|
||
# ━━━ Test Report ━━━
|
||
# ==============================================================================================
|
||
echo ""
|
||
echo "━━━━━ $ICON_SUMMARY FALLBACK TEST REPORT ━━━━━"
|
||
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||
echo "$ICON_HOST Remote: $REMOTE_ID ($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
|
||
warn "DRY RUN — no changes made"
|
||
elif [[ "$FAIL_COUNT" -eq 0 ]]; then
|
||
warn "$ICON_DONE ALL $TOTAL_PHASES PHASES PASSED"
|
||
notify "Fallback test PASSED on $(hostname) — all $TOTAL_PHASES phases completed" \
|
||
"Fallback Test" "normal"
|
||
else
|
||
error "$FAIL_COUNT/$TOTAL_PHASES PHASES FAILED"
|
||
notify "Fallback test FAILED on $(hostname) — $FAIL_COUNT/$TOTAL_PHASES phases failed: ${PHASES_FAIL[*]}" \
|
||
"Fallback Test" "warning"
|
||
fi
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
|
||
[[ "$FAIL_COUNT" -gt 0 ]] && exit 1
|
||
exit 0 |