diff --git a/Tools/mesh_monitor.sh b/Tools/mesh_monitor.sh new file mode 100755 index 0000000..95c6e75 --- /dev/null +++ b/Tools/mesh_monitor.sh @@ -0,0 +1,217 @@ +#!/bin/bash +# ============================================================================================== +# ================================= Mesh Monitor =============================================== +# ============================================================================================== +# +# PURPOSE +# ───────────────────────────────────────────────────────────────────────────── +# At-a-glance view of everyone in the mesh — who they are, how to reach them, +# and what services each node has put up for mutual fallback protection. +# Read-only — sources conf data only, no SSH or API calls, no sensitive data. +# +# ============================================================================================== +# OPERATIONAL MODEL +# ============================================================================================== +# +# Reads directly from all three conf files without detect_hosts(). +# Iterates all defined HOST* vars — scales automatically as new nodes join. +# +# Sections: +# MEMBERS — server name, owner, contact email for each HOST* +# COVERAGE — what each host runs for others when they go down, by tier +# PARTNERSHIP — enabled/disabled, current owner, sync interval +# +# Safe to share with mesh members — contains no API keys, passwords, or SSH keys. +# +# ============================================================================================== +# CONFIGURATION +# ============================================================================================== +# +# host*.conf — per-host identity vars read by this script: +# +# HOST*_OWNER human-readable owner name +# HOST*_OWNER_EMAIL contact email — also used by NPM for Let's Encrypt +# +# ============================================================================================== +# RUNTIME MODES +# ============================================================================================== +# +# mesh_monitor.sh — show full mesh overview +# mesh_monitor.sh --log — verbose output +# mesh_monitor.sh --status — show config and exit +# +# ============================================================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "$SCRIPT_DIR/../load_config.sh" + +parse_args "$@" + +# ── Helpers ─────────────────────────────────────────────────────────────────────────────────── + +# Populate ALL_HOST_IDS with every defined HOST* var +collect_hosts() { + ALL_HOST_IDS=() + for h in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do + [[ -n "${!h:-}" ]] && ALL_HOST_IDS+=("$h") + done +} + +# Read a bash array by computed name — returns space-separated elements +get_array() { eval "echo \"\${${1}[*]}\""; } + +# Convert minutes to human-readable delay string +format_delay() { + local m="${1:-0}" + (( m == 0 )) && { echo "immediate"; return; } + (( m >= 1440 )) && { echo "$((m / 1440))d"; return; } + (( m >= 60 )) && { echo "$((m / 60))hr"; return; } + echo "${m}min" +} + +collect_hosts + +# ============================================================================================== +# ━━━ Status ━━━ +# ============================================================================================== +if [[ "$SHOW_STATUS" == true ]]; then + echo "" + echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━" + echo " Hosts: ${ALL_HOST_IDS[*]}" + for h in "${ALL_HOST_IDS[@]}"; do + owner_var="${h}_OWNER"; owner="${!owner_var:-unknown}" + email_var="${h}_OWNER_EMAIL"; email="${!email_var:-(not set)}" + echo " $h: ${!h} / $owner / $email" + done + echo " Mode: read-only — conf data only, no SSH or API calls" + echo "━━━━━━━━━━━━━━━━━━━━━━━" + exit 0 +fi + +# ============================================================================================== +# ━━━ Mesh Overview ━━━ +# ============================================================================================== + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " MESH OVERVIEW" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# ── Members ─────────────────────────────────────────────────────────────────────────────────── +echo "" +echo "━━━ $ICON_HOST Members ━━━" +echo "" + +# Dynamic column widths +W_HOST=4; W_SERVER=6; W_OWNER=5 +for h in "${ALL_HOST_IDS[@]}"; do + server="${!h}" + owner_var="${h}_OWNER"; owner="${!owner_var:-unknown}" + (( ${#h} > W_HOST )) && W_HOST=${#h} + (( ${#server} > W_SERVER )) && W_SERVER=${#server} + (( ${#owner} > W_OWNER )) && W_OWNER=${#owner} +done + +printf " %-${W_HOST}s %-${W_SERVER}s %-${W_OWNER}s %s\n" \ + "HOST" "SERVER" "OWNER" "EMAIL" +printf " %-${W_HOST}s %-${W_SERVER}s %-${W_OWNER}s %s\n" \ + "$(printf '─%.0s' $(seq 1 $W_HOST))" \ + "$(printf '─%.0s' $(seq 1 $W_SERVER))" \ + "$(printf '─%.0s' $(seq 1 $W_OWNER))" \ + "─────────────────────────" + +for h in "${ALL_HOST_IDS[@]}"; do + server="${!h}" + owner_var="${h}_OWNER"; owner="${!owner_var:-unknown}" + email_var="${h}_OWNER_EMAIL"; email="${!email_var:-(not set)}" + printf " %-${W_HOST}s %-${W_SERVER}s %-${W_OWNER}s %s\n" \ + "$h" "$server" "$owner" "$email" +done + +# ── Protected Services ──────────────────────────────────────────────────────────────────────── +echo "" +echo "━━━ $ICON_SHIELD Protected Services ━━━" + +COVERAGE_FOUND=false + +# Iterate from the perspective of each protected host — what do they want covered +for covered in "${ALL_HOST_IDS[@]}"; do + covered_owner_var="${covered}_OWNER"; covered_owner="${!covered_owner_var:-$covered}" + covered_email_var="${covered}_OWNER_EMAIL"; covered_email="${!covered_email_var:-}" + + # Find who covers this host and collect their tiers + any_tiers=false + declare -A tier_containers=() + declare -A tier_delays=() + covered_by="" + + for covering in "${ALL_HOST_IDS[@]}"; do + [[ "$covering" == "$covered" ]] && continue + + for tier in 1 2 3 4; do + containers=$(get_array "FALLBACK_${covering}_COVERS_${covered}_TIER${tier}") + [[ -z "$containers" ]] && continue + any_tiers=true + tier_containers[$tier]="$containers" + delay_var="${covered}_TIER${tier}_DELAY" + tier_delays[$tier]="${!delay_var:-0}" + done + + [[ "$any_tiers" == true ]] && { + covering_owner_var="${covering}_OWNER" + covering_owner="${!covering_owner_var:-$covering}" + covered_by="$covering ($covering_owner)" + } + done + + [[ "$any_tiers" == false ]] && continue + + COVERAGE_FOUND=true + echo "" + + header="$covered $covered_owner" + [[ -n "$covered_email" ]] && header+=" — $covered_email" + echo " $header" + + for tier in 1 2 3 4; do + [[ -z "${tier_containers[$tier]:-}" ]] && continue + if [[ "$tier" -eq 1 ]]; then + printf " Tier 1 %-12s %s\n" "(immediate)" "${tier_containers[$tier]// /, }" + else + delay=$(format_delay "${tier_delays[$tier]:-0}") + printf " Tier %s %-12s %s\n" "$tier" "($delay)" "${tier_containers[$tier]// /, }" + fi + done + + [[ -n "$covered_by" ]] && echo " Covered by: $covered_by" + + unset tier_containers tier_delays + declare -A tier_containers=() + declare -A tier_delays=() +done + +if [[ "$COVERAGE_FOUND" == false ]]; then + echo "" + echo " No fallback coverage configured" +fi + +# ── Partnership ─────────────────────────────────────────────────────────────────────────────── +echo "" +echo "━━━ $ICON_NET Partnership ━━━" +echo "" + +if [[ "${PARTNERSHIP_ENABLED:-false}" == true ]]; then + owner_host="${PARTNERSHIP_OWNER_HOST:-HOST1}" + owner_server="${!owner_host:-unknown}" + owner_name_var="${owner_host}_OWNER"; owner_name="${!owner_name_var:-unknown}" + echo " Status enabled" + echo " Owner $owner_host ($owner_server / $owner_name)" + echo " Sync every ${PARTNERSHIP_SYNC_INTERVAL:-15}min" +else + echo " Status disabled" +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +exit 0