sync: auto-sync from GURU-5070 at 2026-06-15 18:45:55
Author: Mike Swanson Machine: GURU-5070 Timestamp: 2026-06-15 18:45:55
This commit is contained in:
@@ -76,12 +76,18 @@ vaulted dedicated key `infrastructure/uos-server-ssh-key` (works from any fleet
|
||||
`infrastructure/uos-server-network-api`. See data-access.md "Plane 2".
|
||||
|
||||
## Applying changes — IMPORTANT boundary
|
||||
This skill is **audit + advisory** today. **Writing config to the controller is not wired** and is
|
||||
high-stakes (a bad channel/power/disable push degrades a live facility). When change-application is
|
||||
added it MUST: go through the controller API with an authed account; change **one zone at a time**;
|
||||
capture live `cu_total`/satisfaction **before and after** each change; and never run nightly/auto
|
||||
channel optimization in ultra-dense sites (pin a manual plan). Until then, hand the recommended
|
||||
plan to a tech to apply in the UniFi UI, or get explicit go before any write path is built.
|
||||
Config changes CAN be automated across many APs (no per-AP UI clicking) via the controller REST API
|
||||
(`PUT .../rest/device/<id>` radio_table) — **`scripts/apply-radio.sh`** does this:
|
||||
```bash
|
||||
bash .claude/skills/unifi-wifi/scripts/apply-radio.sh cascades ng power low [--zone "Floor 3"] # DRY-RUN preview
|
||||
```
|
||||
Dry-run prints per-AP before->after + rollback values + the exact REST payload. **Writes are GATED
|
||||
OFF** until (1) a read-WRITE controller admin is vaulted (`infrastructure/uos-server-network-api-rw`;
|
||||
the root SSH key is the data plane, NOT an API write session) and (2) `--apply` is passed — and even
|
||||
then must: capture old values, go **one `--zone` at a time**, validate live `cu_total`/busy% with
|
||||
`watch-ap.sh` **before and after**, and never run nightly/auto channel optimization in ultra-dense
|
||||
sites. `disable` is intentionally NOT in apply-radio.sh yet — it needs the RF-neighbor table to prove
|
||||
redundancy first (see interference-model.md). Get explicit go before enabling any write.
|
||||
|
||||
## Roadmap
|
||||
- **Phase 1 (done):** config + interference audit, flags, methodology. Read-only.
|
||||
|
||||
70
.claude/skills/unifi-wifi/scripts/apply-radio.sh
Normal file
70
.claude/skills/unifi-wifi/scripts/apply-radio.sh
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# apply-radio.sh — compute (and, when enabled, apply) a radio config change across a set of APs.
|
||||
# DRY-RUN BY DEFAULT: prints the exact per-AP before->after, the REST payload that would be sent,
|
||||
# and the captured old values for rollback. Writes are GATED and off until a read-WRITE controller
|
||||
# admin is vaulted AND --apply is passed (production safety; this is a live facility).
|
||||
#
|
||||
# WHY API, not SSH/Mongo: UniFi config is controller-authoritative. The supported write path is the
|
||||
# controller REST API (PUT /proxy/network/api/s/<site>/rest/device/<id>) with an ADMIN SESSION. The
|
||||
# vaulted root SSH key is the data plane (reads/AP-watch), NOT an API write credential. Editing Mongo
|
||||
# + forcing a re-provision is possible with root but unsupported/fragile -- don't, on a live site.
|
||||
#
|
||||
# Usage:
|
||||
# bash .../apply-radio.sh <site> <band ng|na|6e> power <low|medium|high|auto|<dBm>> [--zone "Floor 3"]
|
||||
# (--apply to actually write — refused until infrastructure/uos-server-network-api-rw is vaulted)
|
||||
# Examples:
|
||||
# apply-radio.sh cascades ng power low # preview: drop all 2.4 radios to Low
|
||||
# apply-radio.sh cascades ng power low --zone "Floor 3"# preview: just Floor 3
|
||||
set -euo pipefail
|
||||
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
||||
UOS="$REPO/.claude/scripts/uos-mongo.sh"; VAULT="$REPO/.claude/scripts/vault.sh"
|
||||
SITEARG="${1:?usage: apply-radio.sh <site> <band> power <val> [--zone Z] [--apply]}"; BAND="${2:?band ng|na|6e}"; ACT="${3:?action: power}"; VAL="${4:?value: low|medium|high|auto|<dBm>}"
|
||||
ZONE=""; APPLY=0; shift 4 || true
|
||||
while [ $# -gt 0 ]; do case "$1" in --zone) ZONE="$2"; shift 2;; --apply) APPLY=1; shift;; *) shift;; esac; done
|
||||
case "$BAND" in ng|na|6e) ;; *) echo "band must be ng|na|6e"; exit 1;; esac
|
||||
[ "$ACT" = "power" ] || { echo "[ERROR] only 'power' is implemented (disable needs the RF-neighbor table; see interference-model.md)"; exit 1; }
|
||||
# value -> tx_power_mode (+ tx_power if a dBm number)
|
||||
if [[ "$VAL" =~ ^[0-9]+$ ]]; then MODE="custom"; DBM="$VAL"; else MODE="$VAL"; DBM=""; fi
|
||||
case "$MODE" in low|medium|high|auto|custom) ;; *) echo "value must be low|medium|high|auto|<dBm>"; exit 1;; esac
|
||||
if [[ "$SITEARG" =~ ^[0-9a-f]{24}$ ]]; then SITE="$SITEARG"; else
|
||||
SITE="$(bash "$UOS" --sites 2>/dev/null | grep -vi 'pq.html' | grep -i "$SITEARG" | awk '{print $1}' | head -1)"; fi
|
||||
[ -n "$SITE" ] || { echo "[ERROR] site not found"; exit 1; }
|
||||
echo "[INFO] site=$SITE band=$BAND -> tx_power_mode=$MODE${DBM:+ tx_power=${DBM}dBm}${ZONE:+ zone='$ZONE'} mode=$([ $APPLY = 1 ] && echo APPLY || echo DRY-RUN)"
|
||||
|
||||
# compute the change set from current config (Mongo)
|
||||
cat <<JS | bash "$UOS" 2>&1 | grep -viE 'pq.html|post-quantum|store now|server may need' | tee /tmp/apply_preview.$$
|
||||
var SITE='$SITE',BAND='$BAND',MODE='$MODE',DBM='$DBM',ZONE='$ZONE';
|
||||
function zoneOf(n){var fz=String(n||'').match(/(\d)(?:st|nd|rd|th)\s*floor/i),rm=String(n||'').match(/\b(\d)\d{2}\b/);return fz?('Floor '+fz[1]):(rm?('Floor '+rm[1]):'misc');}
|
||||
var n=0,skip=0;
|
||||
db.device.find({site_id:SITE,type:'uap'},{name:1,radio_table:1}).forEach(function(a){
|
||||
if(ZONE && zoneOf(a.name)!==ZONE) return;
|
||||
(a.radio_table||[]).forEach(function(r){ if(r.radio!==BAND) return;
|
||||
var curMode=r.tx_power_mode, curDbm=(r.tx_power!=null?r.tx_power:'');
|
||||
if(curMode===MODE && (MODE!=='custom' || String(curDbm)===DBM)){ skip++; return; } // already at target
|
||||
n++;
|
||||
print("CHANGE "+(a.name||a._id)+" ["+BAND+"] tx_power_mode: "+curMode+(curMode=='custom'?('('+curDbm+')'):'')+" -> "+MODE+(DBM?('('+DBM+')'):'')
|
||||
+" (rollback: mode="+curMode+(curDbm!==''?(' dbm='+curDbm):'')+")");
|
||||
});
|
||||
});
|
||||
print("\nSUMMARY: "+n+" radios would change, "+skip+" already at target.");
|
||||
print("REST payload per AP (what --apply WOULD PUT to /proxy/network/api/s/<site>/rest/device/<id>):");
|
||||
print(" { \"radio_table\": [ { \"radio\":\""+BAND+"\", \"tx_power_mode\":\""+MODE+"\""+(DBM?(", \"tx_power\":"+DBM):"")+" , ...other fields unchanged... } ] }");
|
||||
JS
|
||||
|
||||
if [ "$APPLY" = "1" ]; then
|
||||
RW_U="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentials.username 2>/dev/null || true)"
|
||||
if [ -z "$RW_U" ]; then
|
||||
echo
|
||||
echo "[BLOCKED] --apply requested but no read-WRITE admin vaulted. This is intentional (live facility)."
|
||||
echo " 1) Create a read-WRITE admin in the UniFi UI (OS Settings -> Admins -> full/site admin)."
|
||||
echo " 2) Vault: bash .claude/skills/vault/scripts/vault-helper.sh new infrastructure/uos-server-network-api-rw \\"
|
||||
echo " --kind generic --name 'UOS Network API (read-write admin)' --tag unifi --set username=<u> --set password=<pw>"
|
||||
echo " 3) Re-run with --apply. (The write path will: capture old values, PUT per AP via the REST API,"
|
||||
echo " and you validate with watch-ap.sh before/after. Roll out per --zone, not site-wide.)"
|
||||
exit 2
|
||||
fi
|
||||
echo "[ERROR] write path is staged but not yet wired in this version — confirm read/watch loop with Howard first, then I'll enable it."
|
||||
exit 3
|
||||
fi
|
||||
echo
|
||||
echo "[dry-run] no changes made. Re-run with --apply once a read-write admin is vaulted (and after the live-watch loop is validated)."
|
||||
Reference in New Issue
Block a user