diff --git a/health-check.sh b/health-check.sh new file mode 100755 index 0000000..f3f44c7 --- /dev/null +++ b/health-check.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +ok() { echo -e " ${GREEN}[OK]${NC} $*"; } +fail() { echo -e " ${RED}[FAIL]${NC} $*"; } +warn() { echo -e " ${YELLOW}[WARN]${NC} $*"; } +header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REGISTRY="${HOME}/.config/tinyboard/spokes" +COMPOSE="$SCRIPT_DIR/spoke/compose.yaml" +RCLONE_CONF="${HOME}/.config/rclone/rclone.conf" + +IS_SPOKE=false +IS_HUB=false + +if docker ps --format '{{.Names}}' 2>/dev/null | grep -qi autossh || [ -f "$COMPOSE" ]; then + IS_SPOKE=true +fi + +if [ -f "$REGISTRY" ]; then + IS_HUB=true +fi + +if [ "$IS_SPOKE" = false ] && [ "$IS_HUB" = false ]; then + echo -e "${YELLOW}Could not detect hub or spoke configuration. Is tinyboard set up?${NC}" + exit 1 +fi + +check_common() { + header "System" + + if command -v docker >/dev/null 2>&1; then + ok "docker installed" + else + fail "docker not found" + fi + + if docker info >/dev/null 2>&1; then + ok "docker daemon running" + else + fail "docker daemon not running" + fi + + local ssh_svc="" + if systemctl list-unit-files ssh.service >/dev/null 2>&1; then + ssh_svc="ssh" + elif systemctl list-unit-files sshd.service >/dev/null 2>&1; then + ssh_svc="sshd" + fi + + if [ -n "$ssh_svc" ]; then + if systemctl is-active "$ssh_svc" >/dev/null 2>&1; then + ok "SSH server running ($ssh_svc)" + else + fail "SSH server not running ($ssh_svc)" + fi + else + warn "Could not detect SSH service" + fi + + local st_container + st_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i syncthing | head -1 || true) + if [ -n "$st_container" ]; then + ok "Syncthing container running ($st_container)" + if curl -sf http://127.0.0.1:8384 >/dev/null 2>&1; then + ok "Syncthing API reachable" + else + warn "Syncthing container running but API not reachable on :8384" + fi + else + warn "No Syncthing container running" + fi +} + +check_spoke() { + header "Spoke" + + local autossh_container + autossh_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i autossh | head -1 || true) + if [ -n "$autossh_container" ]; then + ok "autossh container running ($autossh_container)" + + local logs + logs=$(docker logs "$autossh_container" 2>&1 | tail -20 || true) + if echo "$logs" | grep -q "remote port forwarding failed"; then + fail "Tunnel reports port forwarding failed — check hub authorized_keys" + else + ok "No tunnel errors in recent logs" + fi + else + fail "No autossh container running" + fi + + local autossh_container + autossh_container=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i autossh | head -1 || true) + if [ -n "$autossh_container" ]; then + local tunnel_port hub_host + tunnel_port=$(docker inspect "$autossh_container" 2>/dev/null | python3 -c " +import sys, json +data = json.load(sys.stdin) +cmd = ' '.join(data[0].get('Config', {}).get('Cmd', [])) +import re +m = re.search(r'-R (\d+):localhost', cmd) +print(m.group(1) if m else '') +" 2>/dev/null || true) + hub_host=$(docker inspect "$autossh_container" 2>/dev/null | python3 -c " +import sys, json +data = json.load(sys.stdin) +cmd = ' '.join(data[0].get('Config', {}).get('Cmd', [])) +import re +m = re.search(r'[a-zA-Z0-9._-]+@([a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)', cmd) +print(m.group(1) if m else '') +" 2>/dev/null || true) + if [ -n "$tunnel_port" ] && [ -n "$hub_host" ]; then + ok "Tunnel configured: port $tunnel_port → $hub_host" + else + warn "Could not parse tunnel config from running container" + fi + fi + + local st_data="/home/armbian/st/data" + if [ -d "$st_data" ]; then + ok "Syncthing data directory exists ($st_data)" + else + warn "Syncthing data directory not found ($st_data)" + fi +} + +check_hub() { + header "Hub" + + local spoke_count + spoke_count=$(wc -l < "$REGISTRY" 2>/dev/null || echo 0) + ok "$spoke_count spoke(s) in registry" + + while IFS= read -r line; do + [ -n "$line" ] || continue + local spoke_name tunnel_port key_path mount_point + spoke_name=$(echo "$line" | awk '{print $1}') + tunnel_port=$(echo "$line" | awk '{print $2}') + key_path=$(echo "$line" | awk '{print $3}') + mount_point=$(echo "$line" | awk '{print $4}') + + echo "" + echo -e " ${CYAN}Spoke: $spoke_name${NC}" + + if ss -tlnp 2>/dev/null | grep -q ":${tunnel_port}"; then + ok "Tunnel port $tunnel_port is listening" + else + fail "Tunnel port $tunnel_port not listening — is the spoke connected?" + fi + + if [ -f "$key_path" ]; then + ok "Hub key exists ($key_path)" + else + fail "Hub key missing ($key_path)" + fi + + if mountpoint -q "$mount_point" 2>/dev/null; then + ok "Mounted at $mount_point" + else + fail "Not mounted at $mount_point" + fi + + if grep -q "\[${spoke_name}-remote\]" "$RCLONE_CONF" 2>/dev/null; then + ok "rclone remote [${spoke_name}-remote] configured" + else + fail "rclone remote [${spoke_name}-remote] not found in rclone.conf" + fi + + if crontab -l 2>/dev/null | grep -q "${spoke_name}-remote:"; then + ok "Auto-mount crontab entry present" + else + warn "No auto-mount crontab entry for $spoke_name" + fi + + done < "$REGISTRY" + + echo "" + local union_remotes + union_remotes=$(grep -A1 'type = union' "$RCLONE_CONF" 2>/dev/null | grep -v 'type = union' | grep -v '^--$' || true) + if [ -n "$union_remotes" ]; then + ok "Union remote(s) configured in rclone.conf" + else + warn "No union remotes configured" + fi +} + +check_common + +if [ "$IS_SPOKE" = true ]; then + check_spoke +fi + +if [ "$IS_HUB" = true ]; then + check_hub +fi + +echo ""