sync: auto-sync from HOWARD-HOME at 2026-06-21 13:18:04
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-21 13:18:04
This commit is contained in:
@@ -63,11 +63,14 @@ path is Cascades — override with the script's vault-path arg per client.
|
||||
`pf-set-src <cidr|any>` (port-forwards + associated filter rule) — built, **live-verify pending** (no
|
||||
box with forwards available yet). Filter rules match by `tracker` (the `id` field is empty on 25.07) or
|
||||
exact `descr`. Each write backs up `config.xml` first; writes drive `scripts/pfsense-gwc.php`.
|
||||
- **Dispatch:** `gw-audit.sh`/`gw-control.sh` auto-route the SAME verbs to this SSH backend when a
|
||||
`clients/<slug>/pfsense-firewall` cred is vaulted (dispatch runs before UOS site resolution, so a
|
||||
pfSense-only slug works; pass `--pfsense <slug>` if the UOS site name differs). The REST
|
||||
`pfsense-backend.sh` (`clients/<slug>/pfsense-api`) remains a **dormant fallback** only. Design/verb-map +
|
||||
pfSense PHP gotchas: `references/ROADMAP.md` §E.
|
||||
- **Dispatch + auto-select:** `gw-audit.sh`/`gw-control.sh` route the SAME verbs to this SSH backend three
|
||||
ways: (1) explicit `--pfsense <slug-or-vault-path>`; (2) the site arg is itself a client slug with a vaulted
|
||||
cred; (3) **auto-select** — the resolved UOS site is bound in `references/site-gateways.tsv`, so no
|
||||
`--pfsense` is needed (`gw-control Cascades fw-list` just works). Manage the map with
|
||||
`scripts/gateway-map.sh` (`lookup`/`list`/`validate`/`suggest`); `suggest` lists no-UniFi-gateway sites
|
||||
not yet bound + vaulted creds not yet referenced. The REST `pfsense-backend.sh`
|
||||
(`clients/<slug>/pfsense-api`) remains a **dormant fallback** only. Design/verb-map + pfSense PHP gotchas:
|
||||
`references/ROADMAP.md` §E.
|
||||
- **Per-client requirement:** `watch-ap`/`neighbor-collect`/`survey-collect`/`dfs-check` default the
|
||||
AP device-auth SSH cred to `clients/cascades-tucson/unifi-ap-ssh`; for another client, vault its
|
||||
own `clients/<x>/unifi-ap-ssh` and pass it as the script's vault-path arg.
|
||||
|
||||
@@ -159,8 +159,12 @@ as a dormant alternative (works if a site ever installs the pkg) but is no longe
|
||||
classified UniFi-gateway (model) vs no-UniFi-gateway (pfSense/third-party candidate), plus the list of
|
||||
vaulted `clients/*/pfsense-firewall` creds with resolved host:port (SSH-backend-ready). Generated live
|
||||
(always current), not a static file. Today: 12 UniFi-gw / 36 no-UniFi-gw sites; 1 pfSense cred (Cascades).
|
||||
- [ ] **Auto-select from the map** (driver picks the pfSense cred for a site WITHOUT `--pfsense`): cred-path
|
||||
convention now settled (option A, above) — remaining blocker is just a persisted UOS-site-name→cred binding.
|
||||
- [x] **Auto-select from the map** (2026-06-21): `gw-audit`/`gw-control` now route a site to its pfSense
|
||||
cred automatically (no `--pfsense`) when it's bound in `references/site-gateways.tsv` (keyed on the resolved
|
||||
24-hex UOS site_id; carries an optional port). Manage with `scripts/gateway-map.sh`
|
||||
(`lookup`/`list`/`validate`/`suggest` — suggest cross-refs no-UniFi-gw controller sites + vaulted creds to
|
||||
find unmapped bindings). Validated: `gw-control Cascades fw-list` and `gw-audit Cascades` both auto-dispatch
|
||||
to the pfSense SSH backend. Seeded with Cascades; add rows via `gateway-map.sh suggest`.
|
||||
- [ ] **VPN convergence:** the "Deeper VPN — gateway-hosted VPN server" item (C) is *easier and better* on
|
||||
pfSense (WireGuard/OpenVPN) than on a USG — fold the Grabb-style "retire Windows RRAS PPTP → gateway VPN"
|
||||
play into the pfSense driver from the start.
|
||||
|
||||
95
.claude/skills/unifi-wifi/scripts/gateway-map.sh
Normal file
95
.claude/skills/unifi-wifi/scripts/gateway-map.sh
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
# gateway-map.sh — manage + query the persisted UOS-site -> pfSense-cred map (references/site-gateways.tsv).
|
||||
# This is the "auto-select" half of ROADMAP §E: gw-audit/gw-control call `lookup` to pick the pfSense SSH
|
||||
# backend for a site automatically (no --pfsense). Humans use list/validate/suggest to keep the map honest.
|
||||
#
|
||||
# Usage:
|
||||
# gateway-map.sh lookup <site_id|site_name> # print "<cred_path>\t<port>" for a mapped site (exit 1 if none)
|
||||
# gateway-map.sh list # show the map (with cred-resolves check)
|
||||
# gateway-map.sh validate # check every row: 24-hex site_id + cred resolves in vault
|
||||
# gateway-map.sh suggest # no-UniFi-gw UOS sites NOT yet mapped + unbound pfSense creds
|
||||
set -uo pipefail
|
||||
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
UOS="$REPO/.claude/scripts/uos-mongo.sh"; VAULT="$REPO/.claude/scripts/vault.sh"
|
||||
MAP="$HERE/../references/site-gateways.tsv"
|
||||
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
|
||||
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/gateway-map" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
|
||||
VROOT="${VAULT_ROOT:-$(jq -r '.vault_path // empty' "$REPO/.claude/identity.json" 2>/dev/null)}"
|
||||
[ -n "$VROOT" ] || VROOT="$REPO/../vault"
|
||||
|
||||
ACT="${1:-list}"; ARG="${2:-}"
|
||||
[ -f "$MAP" ] || { echo "[ERROR] map not found: $MAP"; exit 2; }
|
||||
is_hex24(){ printf '%s' "$1" | grep -qE '^[0-9a-f]{24}$'; }
|
||||
cred_ok(){ [ -n "$(bash "$VAULT" get-field "$1" credentials.password 2>/dev/null || bash "$VAULT" get-field "$1" password 2>/dev/null)" ]; }
|
||||
|
||||
# iterate data rows -> calls `row <site_id> <cred> <port> <name>`
|
||||
each_row(){ while IFS=$'\t' read -r sid cred port name; do case "${sid:-}" in ''|'#'*) continue;; esac; "$1" "$sid" "$cred" "${port:-}" "${name:-}"; done < "$MAP"; }
|
||||
|
||||
case "$ACT" in
|
||||
lookup)
|
||||
[ -n "$ARG" ] || { echo "[ERROR] lookup needs <site_id|site_name>" >&2; exit 2; }
|
||||
found=""
|
||||
while IFS=$'\t' read -r sid cred port name; do
|
||||
case "${sid:-}" in ''|'#'*) continue;; esac
|
||||
if [ "$sid" = "$ARG" ]; then found="$cred ${port:-}"; break; fi
|
||||
if ! is_hex24 "$ARG"; then
|
||||
ln=$(printf '%s' "${name:-}" | tr 'A-Z' 'a-z'); lk=$(printf '%s' "$ARG" | tr 'A-Z' 'a-z')
|
||||
[ -n "$ln" ] && case "$ln" in *"$lk"*) found="$cred ${port:-}"; break;; esac
|
||||
fi
|
||||
done < "$MAP"
|
||||
[ -n "$found" ] && { printf '%s\n' "$found"; exit 0; } || exit 1
|
||||
;;
|
||||
|
||||
list)
|
||||
echo "[INFO] gateway map ($MAP):"
|
||||
printf " %-26s %-42s %-5s %s\n" "site_id" "cred_path" "port" "site_name / cred-check"
|
||||
printf " %-26s %-42s %-5s %s\n" "-------" "---------" "----" "---------"
|
||||
_row(){ local sid="$1" cred="$2" port="$3" name="$4" tag
|
||||
if cred_ok "$cred"; then tag="[ok]"; else tag="[MISSING CRED]"; fi
|
||||
printf " %-26s %-42s %-5s %s %s\n" "$sid" "$cred" "${port:--}" "$name" "$tag"; }
|
||||
each_row _row
|
||||
;;
|
||||
|
||||
validate)
|
||||
rc=0
|
||||
_row(){ local sid="$1" cred="$2" port="$3" name="$4"
|
||||
is_hex24 "$sid" || { echo "[BAD] site_id not 24-hex: '$sid' ($name)"; rc=1; return; }
|
||||
if cred_ok "$cred"; then echo "[ok] $name ($sid) -> $cred"; else echo "[MISSING CRED] $name ($sid) -> $cred"; rc=1; fi; }
|
||||
each_row _row
|
||||
[ $rc -eq 0 ] && echo "[OK] all rows valid" || echo "[WARNING] some rows need attention"
|
||||
exit $rc
|
||||
;;
|
||||
|
||||
suggest)
|
||||
echo "[INFO] Cross-referencing controller (no-UniFi-gateway sites) + vault (pfSense creds) vs the map..."
|
||||
# mapped site_ids
|
||||
mapped="$(grep -vE '^\s*#|^\s*$' "$MAP" | cut -f1 | tr '\n' ' ')"
|
||||
# no-UniFi-gateway sites from the controller (site_id<TAB>name)
|
||||
third="$(cat <<'JS' | bash "$UOS" 2>/dev/null | grep -viE 'WARNING|post-quantum|store now|upgraded|openssh'
|
||||
var s={}; db.site.find({},{name:1,desc:1}).forEach(function(x){ s[x._id.str]={n:(x.desc||x.name),gw:0}; });
|
||||
db.device.find({type:{$in:['ugw','uxg','udm','ucg']}},{site_id:1}).forEach(function(d){ if(s[d.site_id])s[d.site_id].gw++; });
|
||||
Object.keys(s).forEach(function(id){ if(s[id].gw===0) print(id+"\t"+s[id].n); });
|
||||
JS
|
||||
)"
|
||||
if [ -z "$third" ]; then echo "[WARNING] no controller data (UOS unreachable?)"; logerr "UOS query returned no sites (suggest)" "uos=$UOS"; fi
|
||||
echo
|
||||
echo " No-UniFi-gateway sites NOT yet in the map (candidates to bind):"
|
||||
printf '%s\n' "$third" | while IFS=$'\t' read -r id nm; do
|
||||
[ -n "$id" ] || continue
|
||||
case " $mapped " in *" $id "*) ;; *) echo " [unmapped] $id $nm";; esac
|
||||
done
|
||||
echo
|
||||
echo " Vaulted pfSense creds (clients/*/pfsense-firewall) and whether a map row references them:"
|
||||
if [ -d "$VROOT" ]; then
|
||||
find "$VROOT/clients" -name 'pfsense-firewall.sops.yaml' 2>/dev/null | sort | while IFS= read -r f; do
|
||||
slug=$(printf '%s' "$f" | sed -E 's#.*/clients/([^/]+)/.*#\1#'); cp="clients/$slug/pfsense-firewall"
|
||||
if grep -qF " $cp " "$MAP" 2>/dev/null || grep -qF " $cp" "$MAP" 2>/dev/null; then echo " [bound] $cp"; else echo " [UNBOUND] $cp (add a row binding a site_id to it)"; fi
|
||||
done
|
||||
fi
|
||||
echo
|
||||
echo " To bind: add a TAB-separated row to $MAP: <site_id>\\t<cred_path>\\t<port|->\\t<name>"
|
||||
;;
|
||||
|
||||
*) echo "usage: gateway-map.sh <lookup|list|validate|suggest> [arg]"; exit 1;;
|
||||
esac
|
||||
@@ -105,20 +105,29 @@ NGW="$(cat "$TMP/ngw" 2>/dev/null || echo 1)"
|
||||
if [ "$NGW" = "0" ]; then
|
||||
# PREFERRED: SSH backend (Mike 2026-06-16). Target = full vault path if --pfsense contains '/'
|
||||
# (option A, Mike 2026-06-21), else a client slug -> clients/<slug>/pfsense-firewall.
|
||||
ssh_target=""
|
||||
ssh_target=""; ssh_port=""
|
||||
# 1. explicit --pfsense full vault path (option A)
|
||||
case "$PFARG" in
|
||||
*/*) if [ -n "$(bash "$VAULT" get-field "$PFARG" credentials.password 2>/dev/null || bash "$VAULT" get-field "$PFARG" password 2>/dev/null)" ]; then ssh_target="$PFARG"; fi ;;
|
||||
esac
|
||||
if [ -z "$ssh_target" ]; then
|
||||
for s in "$PFARG" "$SITEARG"; do
|
||||
[ -n "$s" ] || continue
|
||||
case "$s" in */*) continue;; esac
|
||||
if [ -n "$(bash "$VAULT" get-field "clients/$s/pfsense-firewall" credentials.password 2>/dev/null)" ]; then ssh_target="$s"; break; fi
|
||||
done
|
||||
# 2. explicit --pfsense client slug
|
||||
if [ -z "$ssh_target" ] && [ -n "$PFARG" ]; then case "$PFARG" in
|
||||
*/*) ;; *) [ -n "$(bash "$VAULT" get-field "clients/$PFARG/pfsense-firewall" credentials.password 2>/dev/null)" ] && ssh_target="$PFARG";; esac; fi
|
||||
# 3. AUTO-SELECT from the gateway map (no --pfsense needed): bind by resolved site name/id
|
||||
if [ -z "$ssh_target" ] && mapres="$(bash "$REPO/.claude/skills/unifi-wifi/scripts/gateway-map.sh" lookup "$SITEARG" 2>/dev/null)"; then
|
||||
IFS=$'\t' read -r ssh_target ssh_port <<< "$mapres"
|
||||
[ -n "$ssh_target" ] && echo; [ -n "$ssh_target" ] && echo "[INFO] site in gateway map -> auto-selected pfSense cred '$ssh_target'"
|
||||
fi
|
||||
# 4. SITEARG itself a client slug
|
||||
if [ -z "$ssh_target" ]; then case "$SITEARG" in
|
||||
*/*) ;; *) [ -n "$(bash "$VAULT" get-field "clients/$SITEARG/pfsense-firewall" credentials.password 2>/dev/null)" ] && ssh_target="$SITEARG";; esac; fi
|
||||
if [ -n "$ssh_target" ]; then
|
||||
echo; echo "[INFO] pfSense gateway (SSH backend, target '$ssh_target') -> pfSense gateway audit:"
|
||||
bash "$REPO/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh" "$ssh_target" audit || true
|
||||
if [ -n "$ssh_port" ] && [ "$ssh_port" != "-" ]; then
|
||||
bash "$REPO/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh" "$ssh_target" audit --port "$ssh_port" || true
|
||||
else
|
||||
bash "$REPO/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh" "$ssh_target" audit || true
|
||||
fi
|
||||
else
|
||||
# REST fallback (dormant): only if a pfSense-api cred is vaulted.
|
||||
cands=()
|
||||
|
||||
@@ -86,6 +86,17 @@ if [[ "$SITEARG" =~ ^[0-9a-f]{24}$ ]]; then SITE="$SITEARG"; else
|
||||
[ -n "$SITE" ] || { echo "[ERROR] site not found: $SITEARG"; exit 1; }
|
||||
MONGO_CLEAN='grep -viE pq.html|post-quantum|store now|server may need'
|
||||
|
||||
# ---------- AUTO-SELECT (ROADMAP §E): resolved site is in the gateway map -> pfSense SSH backend ----------
|
||||
# No --pfsense needed: if site-gateways.tsv binds this site_id to a pfSense cred, route the verb there.
|
||||
if mapres="$(bash "$REPO/.claude/skills/unifi-wifi/scripts/gateway-map.sh" lookup "$SITE" 2>/dev/null)"; then
|
||||
IFS=$'\t' read -r mcred mport <<< "$mapres"
|
||||
echo "[INFO] site '$SITEARG' is in the gateway map -> pfSense SSH backend (cred:$mcred) for '$ACT'"
|
||||
args=("$mcred" "$ACT"); [ ${#POS[@]} -gt 0 ] && args+=("${POS[@]}")
|
||||
[ -n "$mport" ] && [ "$mport" != "-" ] && args+=(--port "$mport")
|
||||
[ "$APPLY" = "1" ] && args+=(--apply)
|
||||
exec bash "$REPO/.claude/skills/unifi-wifi/scripts/pfsense-ssh.sh" "${args[@]}"
|
||||
fi
|
||||
|
||||
# ---------- READ-ONLY listers (Mongo, no cred) ----------
|
||||
pf_list() {
|
||||
cat <<JS | bash "$UOS" 2>&1 | grep -viE 'pq.html|post-quantum|store now|server may need'
|
||||
|
||||
@@ -96,5 +96,7 @@ if [ -d "$VROOT" ]; then
|
||||
else
|
||||
echo " (vault not found at $VROOT - set VAULT_ROOT)"
|
||||
fi
|
||||
echo " Bind a no-UniFi-gateway site to its pfSense cred via --pfsense <slug>, e.g.:"
|
||||
echo " bash .claude/skills/unifi-wifi/scripts/gw-audit.sh '<UOS site name>' --pfsense <slug>"
|
||||
echo " One-off: bind via --pfsense <slug-or-vault-path>, e.g. gw-audit '<site>' --pfsense <slug>."
|
||||
echo " Persistent AUTO-SELECT (no --pfsense): add a row to references/site-gateways.tsv —"
|
||||
echo " bash .claude/skills/unifi-wifi/scripts/gateway-map.sh suggest # shows unmapped sites + creds"
|
||||
echo " bash .claude/skills/unifi-wifi/scripts/gateway-map.sh list|validate"
|
||||
|
||||
Reference in New Issue
Block a user