61 lines
4.0 KiB
Bash
61 lines
4.0 KiB
Bash
#!/usr/bin/env bash
|
|
# watch-ap.sh — live, low-latency RF watch of a UniFi AP straight from the AP (no controller lag).
|
|
# SSHes into the AP and streams, per radio, every INTERVAL seconds: channel utilization + clients +
|
|
# tx-retries (from mca-dump) and channel busy% + noise floor (from `iw survey`, diffed). Ideal for
|
|
# watching an AP react to a targeted change (power/channel/neighbor-disable) in seconds.
|
|
#
|
|
# REQUIRES: L3 reach to the AP's mgmt IP. At Cascades the APs are on 192.168.2.x/3.x (mgmt VLANs) —
|
|
# bring up the Cascades VPN first. Device-auth SSH cred is vaulted (clients/cascades-tucson/unifi-ap-ssh).
|
|
# AUTH (UniFi device-auth is password-based): uses `sshpass` if installed, otherwise falls back to
|
|
# OpenSSH's SSH_ASKPASS helper (no sshpass needed). NOTE the fallback uses `ssh` from PATH: on Windows
|
|
# that must be MSYS/Git-bash ssh — Win10/11 system OpenSSH cannot exec a shell askpass (CreateProcessW
|
|
# error 193); on Linux/macOS system ssh works fine. Find AP IPs via:
|
|
# echo 'db.device.find({site_id:"685f39068e65331c46ef6dd2",type:"uap"},{name:1,ip:1}).forEach(printjson)' | bash .claude/scripts/uos-mongo.sh
|
|
#
|
|
# Usage: bash .claude/skills/unifi-wifi/scripts/watch-ap.sh <ap-ip> [interval=2] [vault-path]
|
|
set -euo pipefail
|
|
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
|
VAULT="$REPO/.claude/scripts/vault.sh"
|
|
AP="${1:?usage: watch-ap.sh <ap-ip> [interval] [vault-path]}"; INT="${2:-2}"; VP="${3:-clients/cascades-tucson/unifi-ap-ssh}"
|
|
U="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
|
P="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"
|
|
[ -n "$U" ] && [ -n "$P" ] || { echo "[BLOCKED] no AP device-auth cred at vault:$VP"; echo " Vault this client's cred: bash .claude/skills/vault/scripts/vault-helper.sh new $VP --kind generic --name '<Client> UniFi AP device-auth SSH' --tag unifi --set username=<u> --set password=<pw>"; exit 2; }
|
|
|
|
# Auth method: sshpass if available, else SSH_ASKPASS fallback (no sshpass needed).
|
|
SSH_OPTS=(-o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
|
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
|
|
if command -v sshpass >/dev/null 2>&1; then
|
|
run_ssh() { SSHPASS="$P" sshpass -e ssh "${SSH_OPTS[@]}" "$@"; }
|
|
echo "[INFO] auth: sshpass"
|
|
else
|
|
ASKPASS="$(mktemp)"; printf '#!/bin/sh\nprintf "%%s\\n" "$WATCH_AP_PW"\n' > "$ASKPASS"; chmod +x "$ASKPASS"
|
|
trap 'rm -f "$ASKPASS"' EXIT
|
|
run_ssh() { WATCH_AP_PW="$P" SSH_ASKPASS="$ASKPASS" SSH_ASKPASS_REQUIRE=force DISPLAY="${DISPLAY:-:0}" ssh "${SSH_OPTS[@]}" "$@"; }
|
|
echo "[INFO] auth: SSH_ASKPASS fallback (sshpass not installed)"
|
|
fi
|
|
|
|
echo "[INFO] watching $AP every ${INT}s (Ctrl-C to stop). Needs Cascades VPN reach."
|
|
# Run the sampling loop ON the AP so each tick is one round-trip; mca-dump for cu/clients, iw survey for busy%/noise.
|
|
run_ssh "$U@$AP" "INT=$INT sh -s" <<'REMOTE' 2>&1 | grep -viE 'Warning: Permanently|pq.html'
|
|
# physical radios only — exclude virtual APs (wifi0ap0...) and mld-* (hyphen breaks ${VAR} expansion)
|
|
radios=$(iw dev 2>/dev/null | awk '/Interface/{print $2}' | grep -E '^(wifi[0-9]+|ath[0-9]+)$' || echo "wifi0 wifi1 wifi2")
|
|
prev=""
|
|
while :; do
|
|
ts=$(date +%H:%M:%S)
|
|
# cu_total + num_sta + tx_retries per radio from mca-dump (JSON; parse with grep/sed, no jq on AP)
|
|
dump=$(mca-dump 2>/dev/null)
|
|
for r in $radios; do
|
|
# iw survey: in-use channel active/busy/noise
|
|
surv=$(iw dev "$r" survey dump 2>/dev/null | awk '
|
|
/\[in use\]/{u=1} u&&/frequency/{f=$2} u&&/noise/{n=$2} u&&/channel active time/{a=$4}
|
|
u&&/channel busy time/{b=$4; print f,n,a,b; u=0}')
|
|
set -- $surv; freq="${1:-?}"; noise="${2:-?}"; act="${3:-0}"; busy="${4:-0}"
|
|
key="$r"; pa=$(eval echo \${ACT_$r:-0}); pb=$(eval echo \${BSY_$r:-0})
|
|
da=$((act-pa)); db=$((busy-pb)); pct="?"; [ "$da" -gt 0 ] 2>/dev/null && pct=$((100*db/da))
|
|
eval ACT_$r=$act; eval BSY_$r=$busy
|
|
echo "$ts $r f=${freq} noise=${noise}dBm busy=${pct}%"
|
|
done
|
|
sleep "${INT:-2}"
|
|
done
|
|
REMOTE
|