2026-04-19 15:41:50 -07:00
|
|
|
#!/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)"
|
2026-04-19 22:35:42 -07:00
|
|
|
HUB_USER="${SUDO_USER:-${USER}}"
|
|
|
|
|
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ]; then
|
|
|
|
|
HUB_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
|
|
|
|
|
else
|
|
|
|
|
HUB_HOME="$HOME"
|
|
|
|
|
fi
|
|
|
|
|
if [ "$(id -u)" -eq 0 ] && [ "$HUB_HOME" = "/root" ]; then
|
|
|
|
|
for u in armbian; do
|
|
|
|
|
CANDIDATE=$(getent passwd "$u" 2>/dev/null | cut -d: -f6)
|
|
|
|
|
if [ -f "${CANDIDATE}/.config/tinyboard/spokes" ]; then
|
|
|
|
|
HUB_HOME="$CANDIDATE"
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
fi
|
|
|
|
|
REGISTRY="${HUB_HOME}/.config/tinyboard/spokes"
|
2026-04-19 15:41:50 -07:00
|
|
|
COMPOSE="$SCRIPT_DIR/spoke/compose.yaml"
|
2026-04-19 22:35:42 -07:00
|
|
|
RCLONE_CONF="${HUB_HOME}/.config/rclone/rclone.conf"
|
2026-04-19 15:41:50 -07:00
|
|
|
|
|
|
|
|
IS_SPOKE=false
|
|
|
|
|
IS_HUB=false
|
|
|
|
|
|
2026-04-19 22:29:02 -07:00
|
|
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qi autossh; then
|
2026-04-19 15:41:50 -07:00
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-04-19 22:34:21 -07:00
|
|
|
if [ "$IS_SPOKE" = true ]; then
|
|
|
|
|
if command -v docker >/dev/null 2>&1; then
|
|
|
|
|
ok "docker installed"
|
2026-04-19 15:41:50 -07:00
|
|
|
else
|
2026-04-19 22:34:21 -07:00
|
|
|
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 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"
|
2026-04-19 15:41:50 -07:00
|
|
|
fi
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-04-19 22:36:46 -07:00
|
|
|
if crontab -u "$(basename "$HUB_HOME")" -l 2>/dev/null | grep -q "${spoke_name}-remote:"; then
|
2026-04-19 15:41:50 -07:00
|
|
|
ok "Auto-mount crontab entry present"
|
|
|
|
|
else
|
|
|
|
|
warn "No auto-mount crontab entry for $spoke_name"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
done < "$REGISTRY"
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
check_common
|
|
|
|
|
|
|
|
|
|
if [ "$IS_SPOKE" = true ]; then
|
|
|
|
|
check_spoke
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ "$IS_HUB" = true ]; then
|
|
|
|
|
check_hub
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo ""
|