Headers claimed protections the code never had, and several destructive paths had no guard against a collapsed config value.
256 lines
11 KiB
Bash
Executable File
256 lines
11 KiB
Bash
Executable File
#!/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.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Read-Only, No Network Calls
|
|
# All data comes from conf files — no SSH, no API calls, no pings. The output
|
|
# is always instant and never fails due to a node being unreachable. This makes
|
|
# it safe to run at any time without side effects.
|
|
#
|
|
# Scales Automatically
|
|
# Iterates all defined HOST* vars rather than a hardcoded list. Adding a new
|
|
# node to master.conf/host*.conf makes it appear in the output immediately.
|
|
#
|
|
# Safe to Share
|
|
# Output contains only identity and coverage configuration — no API keys,
|
|
# no passwords, no SSH keys. The report can be shared with other mesh members
|
|
# without exposing secrets.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# No External Dependencies
|
|
# Reads only from already-sourced conf files. No curl, no ssh, no docker —
|
|
# nothing that can fail, hang, or require credentials.
|
|
#
|
|
# Empty Mesh Guard
|
|
# collect_hosts() populates ALL_HOST_IDS — if no HOST* vars are defined the
|
|
# output sections iterate over an empty array and exit cleanly.
|
|
#
|
|
# No Root, No Lock, No detect_hosts — Deliberate
|
|
# This is the one script in the ecosystem that intentionally omits all three, and
|
|
# they should not be added. It writes nothing, so there is no state for a lock to
|
|
# protect and no privileged operation to justify a root gate. It reports on every
|
|
# node rather than acting as one, so detect_hosts() would narrow it to this host's
|
|
# aliases — the opposite of what it is for. Every HOST* var is read directly instead.
|
|
#
|
|
# ==============================================================================================
|
|
# 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
|
|
log "$ICON_HOST Hosts: ${ALL_HOST_IDS[*]} (${#ALL_HOST_IDS[@]} in mesh)"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ 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"
|
|
log "$h: server=$server owner=$owner email=$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:-}"
|
|
|
|
# Each host defines its own recovery profile — read directly from covered host's vars
|
|
any_tiers=false
|
|
declare -A tier_containers=()
|
|
declare -A tier_delays=()
|
|
covered_by=""
|
|
|
|
for tier in 1 2 3 4; do
|
|
containers=$(get_array "FALLBACK_${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
|
|
|
|
if [[ "$any_tiers" == true ]]; then
|
|
for covering in "${ALL_HOST_IDS[@]}"; do
|
|
[[ "$covering" == "$covered" ]] && continue
|
|
covering_owner_var="${covering}_OWNER"
|
|
covering_owner="${!covering_owner_var:-$covering}"
|
|
covered_by="${covered_by:+$covered_by, }$covering ($covering_owner)"
|
|
done
|
|
fi
|
|
|
|
[[ "$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
|