76 lines
4.2 KiB
Bash
76 lines
4.2 KiB
Bash
#!/usr/bin/env bash
|
|
# live-stats.sh — Plane-2 live RF/airtime from the UOS Network API (classic session API).
|
|
# Gives CURRENT per-AP per-radio cu_total / cu_self / num_sta / satisfaction / tx_retries and the
|
|
# AP RF-neighbor table — for before/after validation of changes and (the neighbor table) the
|
|
# materials-aware AP-to-AP coverage graph that unlocks confident radio DISABLES.
|
|
#
|
|
# AUTH (provision once): the classic API needs a controller admin session. Create a dedicated
|
|
# READ-ONLY admin in the UniFi UI (OS Settings -> Admins -> add a Viewer), then vault it:
|
|
# bash .claude/skills/vault/scripts/vault-helper.sh new infrastructure/uos-server-network-api \
|
|
# --kind generic --name "UOS Network API (read-only admin)" --tag unifi \
|
|
# --set username=<viewer> --set password=<pw>
|
|
# (A UniFi Network Integration API key also works for /proxy/network/integration/v1, but the rich
|
|
# radio_table_stats + RF-neighbor data live in the classic /proxy/network/api path used here.)
|
|
#
|
|
# Usage: bash .claude/skills/unifi-wifi/scripts/live-stats.sh <site-name|short> [--clients]
|
|
set -euo pipefail
|
|
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
|
VAULT="$REPO/.claude/scripts/vault.sh"
|
|
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
|
|
SITEARG="${1:?usage: live-stats.sh <site-name|short> [--clients]}"; WANT_CLIENTS="${2:-}"
|
|
|
|
# Prefer the read-only admin; fall back to the read-write one (a single admin covers both).
|
|
U="$(bash "$VAULT" get-field infrastructure/uos-server-network-api credentials.username 2>/dev/null || true)"
|
|
P="$(bash "$VAULT" get-field infrastructure/uos-server-network-api credentials.password 2>/dev/null || true)"
|
|
if [ -z "$U" ] || [ -z "$P" ]; then
|
|
U="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentials.username 2>/dev/null || true)"
|
|
P="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentials.password 2>/dev/null || true)"
|
|
fi
|
|
if [ -z "$U" ] || [ -z "$P" ]; then
|
|
echo "[BLOCKED] No controller credential vaulted yet. Provision a read-only admin and vault it:"
|
|
sed -n '8,14p' "$0"
|
|
exit 2
|
|
fi
|
|
|
|
CJ="$(mktemp)"; trap 'rm -f "$CJ"' EXIT
|
|
base="https://$HOST:$PORT"
|
|
# UniFi OS login -> session cookie
|
|
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" \
|
|
-H 'Content-Type: application/json' --data-binary "$(python -c 'import json,os;print(json.dumps({"username":os.environ["U"],"password":os.environ["P"]}))' U="$U" P="$P")")
|
|
[ "$code" = "200" ] || { echo "[ERROR] login HTTP $code"; exit 1; }
|
|
|
|
# resolve site short name (classic API keys on the short name, not the _id)
|
|
SHORT="$SITEARG"
|
|
if [[ "$SITEARG" =~ ^[0-9a-f]{24}$ || ! "$SITEARG" =~ ^[a-z0-9]{8}$ ]]; then
|
|
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "
|
|
import sys,json
|
|
d=json.load(sys.stdin).get('data',[])
|
|
q='''$SITEARG'''.lower()
|
|
for s in d:
|
|
if s.get('_id')=='''$SITEARG''' or q in (s.get('desc','').lower()): print(s.get('name')); break
|
|
" 2>/dev/null)"
|
|
fi
|
|
[ -n "$SHORT" ] || { echo "[ERROR] could not resolve site '$SITEARG'"; exit 1; }
|
|
echo "[INFO] site short=$SHORT"
|
|
|
|
curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/device" | python -c "
|
|
import sys,json
|
|
for d in json.load(sys.stdin).get('data',[]):
|
|
if d.get('type')!='uap': continue
|
|
print('AP',d.get('name'),'clients=',d.get('num_sta'))
|
|
for r in d.get('radio_table_stats',[]):
|
|
print(' ',r.get('radio'),'ch',r.get('channel'),'cu_total',r.get('cu_total'),'cu_self_rx',r.get('cu_self_rx'),'cu_self_tx',r.get('cu_self_tx'),'num_sta',r.get('num_sta'),'tx_retries',r.get('tx_retries'),'satisfaction',r.get('satisfaction'))
|
|
# RF neighbor table (materials-aware AP-to-AP visibility) if present
|
|
for n in (d.get('radio_table') or []):
|
|
pass
|
|
" 2>&1 | head -60
|
|
|
|
if [ "$WANT_CLIENTS" = "--clients" ]; then
|
|
echo "=== clients (rssi/rate/retries) ==="
|
|
curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/sta" | python -c "
|
|
import sys,json
|
|
for c in json.load(sys.stdin).get('data',[])[:40]:
|
|
print(' ',c.get('hostname') or c.get('mac'),'ap',c.get('ap_mac'),'rssi',c.get('rssi'),'signal',c.get('signal'),'tx_rate',c.get('tx_rate'),'retries',c.get('tx_retries'),'sat',c.get('satisfaction'))
|
|
" 2>&1 | head -45
|
|
fi
|