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
RoadChain-SHA2048: 72a4221bc7aa108b RoadChain-Identity: alexa@sovereign RoadChain-Full: 72a4221bc7aa108b2cf16b6390e819405927c1e6bb6f41d81bc9738ab083c4c198fd434edbb4c4025f5688394ab6fd7421b9d858fab3ad4c2fad9be6e44b181243906421c7ca51504ab1f2025c4d9b100ad62ebe65cd2b05724adf48383e5aafbfe3541c620e19080101d90b876df22da8f57947109bb43db501099de77af6298d33b5257edbf651696455026a470715ee152bafcdfb5d6906dbdeaddbbb9622fccc44f2e53474eff6a7b436dd177fd2fd84a3ef0b68b534df0c139e94ae95e3f4119bf73d7080885538269f19592274fc3f29c66b53154e53cc0024d8f18de8a9929acdb330a88697874d864977fe14e4635449afacffc5bbc06c69ab26aa86
136 lines
6.4 KiB
Bash
Executable File
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."
|