#!/usr/bin/env bash # gw-audit.sh — gateway / WAN / internet + overall site-health audit (controller-side, read-only). # Uses the controller's own stat/health (wan/www/wlan/lan/vpn subsystems) + the gateway device's # per-WAN detail. Reports WAN status + IP + uplink speed, internet latency/drops/last-speedtest, # gateway CPU/mem/uptime, and the adoption/health rollup (APs/switches adopted vs disconnected vs # pending, client counts). Flags: WAN/internet not-ok, high latency/drops, disconnected or pending # devices, gateway resource pressure. No AP cred / VPN needed -> any UOS site. (ROADMAP C) # # Sites with a third-party firewall (e.g. pfSense) show num_gw=0 -> "no UniFi gateway" (still reports # wlan/lan/adoption health). # # Usage: bash .claude/skills/unifi-wifi/scripts/gw-audit.sh set -uo pipefail REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)" UOS="$REPO/.claude/scripts/uos-mongo.sh"; VAULT="$REPO/.claude/scripts/vault.sh" HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}" SITEARG="${1:?usage: gw-audit.sh [--pfsense ]}"; shift || true PFARG="" # optional: pfSense client slug (or full vault path) when UOS site name != client slug while [ $# -gt 0 ]; do case "$1" in --pfsense) PFARG="${2:?--pfsense needs a slug/vault-path}"; shift 2;; *) shift;; esac; done TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT U="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentials.username 2>/dev/null)" P="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentials.password 2>/dev/null)" [ -n "$U" ] && [ -n "$P" ] || { echo "[ERROR] no controller cred (infrastructure/uos-server-network-api-rw)"; exit 1; } base="https://$HOST:$PORT"; CJ="$TMP/cj" 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,sys;print(json.dumps({"username":sys.argv[1],"password":sys.argv[2]}))' "$U" "$P")") [ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; exit 1; } 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 s.get('name')=='''$SITEARG''' or q in (s.get('desc','').lower()): print(s.get('name')); break ")"; [ -n "$SHORT" ] || SHORT="$SITEARG" echo "[INFO] gateway/health audit: site=$SHORT" curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/health" -o "$TMP/health.json" curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/device" -o "$TMP/dev.json" export NGW_FILE="$TMP/ngw" python - "$TMP/health.json" "$TMP/dev.json" <<'PY' import sys,json,os H={h['subsystem']:h for h in json.load(open(sys.argv[1])).get('data',[])} devs=json.load(open(sys.argv[2])).get('data',[]) flags=[] def st(x): return (x or 'unknown') wan=H.get('wan',{}); www=H.get('www',{}); wlan=H.get('wlan',{}); lan=H.get('lan',{}); vpn=H.get('vpn',{}) print("\n== WAN / Internet ==") ngw=wan.get('num_gw',0) try: open(os.environ['NGW_FILE'],'w').write(str(ngw)) except Exception: pass if not ngw: print(" no UniFi gateway at this site (third-party firewall, e.g. pfSense)") else: gwdev=next((d for d in devs if d.get('type') in ('ugw','uxg','udm')),None) gwname=wan.get('gw_name') or (gwdev and (gwdev.get('name') or gwdev.get('model'))) or '?' gwmodel=(gwdev and gwdev.get('model')) or '' print(f" gateway: {gwname} [{gwmodel}] ({wan.get('gw_version')}) status={st(wan.get('status'))}") print(f" WAN IP: {wan.get('wan_ip')} gateways: {wan.get('gateways')} nameservers: {wan.get('nameservers')}") gs=wan.get('gw_system-stats') or {} if gs: print(f" gw load: cpu={gs.get('cpu')}% mem={gs.get('mem')}% uptime={int(int(gs.get('uptime',0))/86400)}d") if st(wan.get('status'))!='ok': flags.append(f"WAN status={wan.get('status')}") try: if float(gs.get('cpu',0))>85: flags.append(f"gateway CPU {gs.get('cpu')}%") if float(gs.get('mem',0))>90: flags.append(f"gateway MEM {gs.get('mem')}%") except: pass # internet (www) print(f" internet: status={st(www.get('status'))} latency={www.get('latency')}ms drops={www.get('drops')} " f"speedtest={www.get('xput_down')}/{www.get('xput_up')} Mbps ping={www.get('speedtest_ping')}ms") if ngw and www and st(www.get('status'))!='ok': flags.append(f"internet(www) status={www.get('status')}") try: if www.get('latency') and float(www.get('latency'))>80: flags.append(f"internet latency {www.get('latency')}ms") if www.get('drops') and float(www.get('drops'))>5: flags.append(f"internet drops={www.get('drops')}") except: pass # per-WAN device detail (multi-WAN) for d in devs: if d.get('type') in ('ugw','uxg','udm'): for wk in ('wan1','wan2'): w=d.get(wk) or {} if w.get('enable'): print(f" {wk}: ip={w.get('ip')} up={w.get('up')} {w.get('speed')}M{'' if w.get('full_duplex',True) else ' HALF-DUPLEX'}") if w.get('enable') and not w.get('up'): flags.append(f"{wk} DOWN") print("\n== Adoption / device health ==") for label,sub in (('APs (wlan)',wlan),('switches (lan)',lan),('gateway (wan)',wan)): a=sub.get('num_adopted'); dc=sub.get('num_disconnected'); pe=sub.get('num_pending') if a is None and dc is None: continue print(f" {label}: adopted={a} disconnected={dc} pending={pe}") if dc: flags.append(f"{label}: {dc} disconnected") if pe: flags.append(f"{label}: {pe} pending adoption") print(f" clients: users={wlan.get('num_user')} guests={wlan.get('num_guest')} iot={wlan.get('num_iot')}") # outdated firmware (from device list) outd=[d.get('name') for d in devs if d.get('upgradable')] if outd: flags.append(f"{len(outd)} device(s) with firmware upgrade available") print("\n== FLAGS ==") if flags: for f in flags: print(f" [!] {f}") else: print(" [OK] gateway/WAN/adoption all healthy") PY # ---------- pfSense gateway augmentation (ROADMAP §E: dispatch when there's no UniFi gateway) ---------- # A pfSense site shows num_gw=0 above (no UniFi gateway). If a pfSense REST API cred is vaulted, run the # pfSense gateway/WAN/DHCP audit via the backend so one `gw-audit ` covers either gateway vendor. NGW="$(cat "$TMP/ngw" 2>/dev/null || echo 1)" if [ "$NGW" = "0" ]; then cands=() [ -n "$PFARG" ] && { case "$PFARG" in */*) cands+=("$PFARG");; *) cands+=("clients/$PFARG/pfsense-api");; esac; } cands+=("clients/$SITEARG/pfsense-api") pf_vp="" for cand in "${cands[@]}"; do if [ -n "$(bash "$VAULT" get-field "$cand" credentials.apikey 2>/dev/null || bash "$VAULT" get-field "$cand" apikey 2>/dev/null)" ]; then pf_vp="$cand"; break; fi done if [ -n "$pf_vp" ]; then echo; echo "[INFO] pfSense gateway cred found (vault:$pf_vp) -> pfSense gateway audit:" bash "$REPO/.claude/skills/unifi-wifi/scripts/pfsense-backend.sh" "$pf_vp" audit || true else echo; echo "[INFO] gateway is third-party (pfSense?). For pfSense WAN/DHCP/firewall audit, vault an API" echo " cred then re-run (pass --pfsense if the UOS site name differs from the client slug):" echo " bash .claude/skills/unifi-wifi/scripts/pfsense-backend.sh clients//pfsense-api setup" fi fi