Files
blackroad/workers/stats-collect.sh
Alexa Amundson ca0d622681
Some checks failed
Lint & Format / detect (push) Failing after 35s
Monorepo Lint / lint-shell (push) Failing after 38s
Monorepo Lint / lint-js (push) Failing after 27s
Lint & Format / js-lint (push) Has been skipped
Lint & Format / py-lint (push) Has been skipped
Lint & Format / sh-lint (push) Has been skipped
Lint & Format / go-lint (push) Has been skipped
sync: 2026-03-15 21:00 — 17 files from Alexandria
RoadChain-SHA2048: 72a4221bc7aa108b
RoadChain-Identity: alexa@sovereign
RoadChain-Full: 72a4221bc7aa108b2cf16b6390e819405927c1e6bb6f41d81bc9738ab083c4c198fd434edbb4c4025f5688394ab6fd7421b9d858fab3ad4c2fad9be6e44b181243906421c7ca51504ab1f2025c4d9b100ad62ebe65cd2b05724adf48383e5aafbfe3541c620e19080101d90b876df22da8f57947109bb43db501099de77af6298d33b5257edbf651696455026a470715ee152bafcdfb5d6906dbdeaddbbb9622fccc44f2e53474eff6a7b436dd177fd2fd84a3ef0b68b534df0c139e94ae95e3f4119bf73d7080885538269f19592274fc3f29c66b53154e53cc0024d8f18de8a9929acdb330a88697874d864977fe14e4635449afacffc5bbc06c69ab26aa86
2026-03-15 21:00:01 -05:00

136 lines
6.4 KiB
Bash
Executable File

#!/bin/bash
# BlackRoad Fleet Collector — SSHes into nodes, collects real data, pushes to stats API
# Runs from Mac cron every 5 minutes
set -e
STATS_URL="https://stats-blackroad.amundsonalexa.workers.dev"
STATS_KEY="blackroad-stats-push-2026"
SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no -o BatchMode=yes"
# ── Probe a single node via single SSH call ──
probe_node() {
local name=$1 host=$2 user=$3
if ! ssh $SSH_OPTS "${user}@${host}" "true" 2>/dev/null; then
echo "{\"name\":\"${name}\",\"host\":\"${host}\",\"status\":\"offline\",\"cpu_temp\":0,\"cpu_pct\":0,\"mem_total_mb\":0,\"mem_used_mb\":0,\"disk_pct\":0,\"ollama_models\":0,\"docker_containers\":0,\"tcp_ports\":0,\"services\":\"\"}"
return
fi
# Single SSH call — collect everything as pipe-delimited
local raw
raw=$(ssh $SSH_OPTS "${user}@${host}" '
# CPU temp
t=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0)
temp=$((t / 1000))
# CPU usage
cpu=$(grep "cpu " /proc/stat | awk "{u=\$2+\$4; tot=u+\$5; print int(u*100/tot)}" 2>/dev/null || echo 0)
# Memory
mem_t=$(free -m 2>/dev/null | awk "/Mem:/{print \$2}")
mem_u=$(free -m 2>/dev/null | awk "/Mem:/{print \$3}")
# Disk
disk=$(df / 2>/dev/null | awk "NR==2{gsub(/%/,\"\"); print \$5}")
# Uptime seconds
up_s=$(awk "{print int(\$1)}" /proc/uptime 2>/dev/null || echo 0)
# Ollama models
models=$(curl -s --connect-timeout 2 http://localhost:11434/api/tags 2>/dev/null | python3 -c "import sys,json;print(len(json.load(sys.stdin).get(\"models\",[])))" 2>/dev/null || echo 0)
# Docker containers
dock=$(docker ps -q 2>/dev/null | wc -l | tr -d " " || echo 0)
# TCP listening ports
ports=$(ss -tlnp 2>/dev/null | tail -n +2 | wc -l | tr -d " " || echo 0)
# Key services
svcs=""
for sp in ssh:22 dns:53 ollama:11434 nginx:80 postgres:5432 minio:9000 gitea:3100 nats:4222 portainer:9443 stats-proxy:7890 cloudflared:20241; do
n=${sp%%:*}; p=${sp##*:}
ss -tlnp 2>/dev/null | grep -q ":${p} " && svcs="${svcs}${n},"
done
printf "%s|%s|%s|%s|%s|%s|%s|%s|%s|%s" "$temp" "$cpu" "$mem_t" "$mem_u" "$disk" "$up_s" "$models" "$dock" "$ports" "$svcs"
' 2>/dev/null) || raw="0|0|0|0|0|0|0|0|0|"
IFS='|' read -r cpu_temp cpu_pct mem_t mem_u disk up_s models dock ports svcs <<< "$raw"
# Convert uptime seconds to human readable
local days=$((up_s / 86400))
local hours=$(( (up_s % 86400) / 3600 ))
local uptime_str="${days}d ${hours}h"
echo "{\"name\":\"${name}\",\"host\":\"${host}\",\"status\":\"online\",\"uptime\":\"${uptime_str}\",\"cpu_temp\":${cpu_temp:-0},\"cpu_pct\":${cpu_pct:-0},\"mem_total_mb\":${mem_t:-0},\"mem_used_mb\":${mem_u:-0},\"disk_pct\":${disk:-0},\"uptime_seconds\":${up_s:-0},\"ollama_models\":${models:-0},\"docker_containers\":${dock:-0},\"tcp_ports\":${ports:-0},\"services\":\"${svcs}\"}"
}
echo "[$(date)] Starting fleet collection..."
# ── Collect sequentially (parallel subshells can't return values easily) ──
alice=$(probe_node "Alice" "192.168.4.49" "pi")
echo " Alice: $(echo "$alice" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['status'],str(d.get('cpu_temp',0))+'°C' if d['status']=='online' else '')" 2>/dev/null)"
cecilia=$(probe_node "Cecilia" "192.168.4.96" "blackroad")
echo " Cecilia: $(echo "$cecilia" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['status'],str(d.get('cpu_temp',0))+'°C' if d['status']=='online' else '')" 2>/dev/null)"
octavia=$(probe_node "Octavia" "192.168.4.101" "pi")
echo " Octavia: $(echo "$octavia" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["status"])' 2>/dev/null)"
aria=$(probe_node "Aria" "192.168.4.98" "blackroad")
echo " Aria: $(echo "$aria" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["status"])' 2>/dev/null)"
lucidia=$(probe_node "Lucidia" "192.168.4.38" "octavia")
echo " Lucidia: $(echo "$lucidia" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['status'],str(d.get('cpu_temp',0))+'°C' if d['status']=='online' else '')" 2>/dev/null)"
# Count online + aggregate
online=0; total_models=0; total_ports=0; total_containers=0
for nd in "$alice" "$cecilia" "$octavia" "$aria" "$lucidia"; do
if echo "$nd" | grep -q '"online"'; then
online=$((online + 1))
m=$(echo "$nd" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("ollama_models",0))' 2>/dev/null) || m=0
p=$(echo "$nd" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("tcp_ports",0))' 2>/dev/null) || p=0
c=$(echo "$nd" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("docker_containers",0))' 2>/dev/null) || c=0
total_models=$((total_models + m))
total_ports=$((total_ports + p))
total_containers=$((total_containers + c))
fi
done
# ── Push fleet data ──
fleet_payload="{\"category\":\"fleet\",\"data\":{\"nodes\":[${alice},${cecilia},${octavia},${aria},${lucidia}],\"online\":${online},\"total\":5,\"total_ollama_models\":${total_models},\"total_tcp_ports\":${total_ports},\"total_containers\":${total_containers}}}"
curl -s -X POST "${STATS_URL}/push?key=${STATS_KEY}" \
-H "Content-Type: application/json" \
-d "$fleet_payload" > /dev/null
echo "[$(date)] Fleet: ${online}/5 nodes online, ${total_models} Ollama models, ${total_ports} TCP ports"
# ── Push infra counts (aggregate live + known static) ──
infra_payload="{\"category\":\"infra\",\"data\":{
\"edge_nodes\":5,
\"online_nodes\":${online},
\"droplets\":2,
\"cf_tunnels\":18,
\"cf_pages\":95,
\"cf_d1\":8,
\"cf_kv\":40,
\"cf_r2\":10,
\"domains\":48,
\"tunnel_hostnames\":100,
\"sqlite_dbs\":228,
\"tops_compute\":52,
\"ollama_models\":${total_models},
\"open_ports\":${total_ports},
\"docker_containers\":${total_containers},
\"gitea_repos\":207,
\"shell_scripts\":92,
\"cron_jobs\":13,
\"wireguard_peers\":6
}}"
curl -s -X POST "${STATS_URL}/push?key=${STATS_KEY}" \
-H "Content-Type: application/json" \
-d "$infra_payload" > /dev/null
# ── Push analytics data (from analytics worker) ──
ANALYTICS=$(curl -s "https://analytics-blackroad.amundsonalexa.workers.dev/stats?range=24h" 2>/dev/null)
if echo "$ANALYTICS" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
curl -s -X POST "${STATS_URL}/push?key=${STATS_KEY}" \
-H "Content-Type: application/json" \
-d "{\"category\":\"analytics\",\"data\":${ANALYTICS}}" > /dev/null
fi
echo "[$(date)] Collection complete."