From 3bdbfc3424ca81db39fb132afc0a072ea10f02f5 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Tue, 21 Apr 2026 14:04:50 -0700 Subject: [PATCH] scripts/discover-unifi: switch to Site Manager API (developer.ui.com) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite to use Ubiquiti's public cloud API at api.ui.com instead of logging into individual controllers via session cookies. Benefits: - One API key covers every UniFi OS device on the account (no per-controller login logic, no cookie jar lifecycle). - Read-only by design (auth keys are scoped). - Works across sites transparently. Three endpoints wired up: hosts (controllers / Cloud Keys), sites, and devices (APs / switches). Each emits a distinct TSV shape so the output can be concatenated and still parsed. `all` mode runs all three and prints section markers on stderr so the stdout stream stays clean TSV suitable for discover-gaps.sh. Pagination handled via nextToken. Rate limit not enforced locally; Ubiquiti documents generous defaults for read endpoints. Note: Site Manager API (early access) doesn't appear to expose a connected-client list directly. For endpoint discovery (IP + MAC of connected clients like laptops, IoT, etc.) we'd still need to hit each local controller's REST API — follow-up if the infrastructure- level data isn't enough. Requires: curl (present), jq (apt install jq). --- scripts/discover-unifi.sh | 206 +++++++++++++++++++++++++++----------- 1 file changed, 147 insertions(+), 59 deletions(-) diff --git a/scripts/discover-unifi.sh b/scripts/discover-unifi.sh index 78a25ef..e3a9f9d 100755 --- a/scripts/discover-unifi.sh +++ b/scripts/discover-unifi.sh @@ -1,80 +1,168 @@ #!/usr/bin/env bash -# discover-unifi.sh — pull client list from a UniFi Controller (UDM / UniFi OS). +# discover-unifi.sh — pull UniFi infrastructure via the Site Manager API +# https://api.ui.com (docs: https://developer.ui.com/site-manager/) # -# Uses the UniFi REST API: -# POST /api/login {username, password} -# GET /proxy/network/api/s//stat/sta +# Uses the cloud-hosted API with a Bearer-key header instead of logging +# into individual controllers directly. One script covers every UniFi +# host registered to the Ubiquiti account that issued the API key. # -# UniFi OS cookie-auth variant (post-2021 devices / UDM / Cloud Key Gen2+). -# For legacy standalone controllers (self-hosted UniFi Network software on -# Linux), the endpoint is /api/login and /api/s//stat/sta directly -# (no /proxy/network prefix). Script tries UniFi OS first, falls back. +# Auth: X-API-KEY header with a key generated in the Ubiquiti console +# (account settings → API keys). Store it outside version control — +# pass via env var UNIFI_API_KEY or a protected env file. # # Usage: -# UNIFI_USER=admin UNIFI_PASS=... scripts/discover-unifi.sh +# UNIFI_API_KEY= scripts/discover-unifi.sh hosts +# UNIFI_API_KEY= scripts/discover-unifi.sh sites +# UNIFI_API_KEY= scripts/discover-unifi.sh devices +# UNIFI_API_KEY= scripts/discover-unifi.sh all # dumps all three as TSVs # -# Optional env: -# UNIFI_SITE default: default -# UNIFI_PORT default: 443 +# Alternatively, put the key in /etc/unifi-api.env (root:600) with +# UNIFI_API_KEY=... +# and source it before calling. # -# Output: TSV on stdout, one client per line: -# IP MAC HOSTNAME AP_ALIAS SOURCE +# Output: TSV on stdout. Column layout depends on the endpoint: +# +# hosts: host_id host_name host_ip model_name controller_version +# sites: site_id site_name host_id description +# devices: mac ip name model site_id host_id +# +# Each row ends with a SOURCE column so outputs can be concatenated and +# still distinguished: +# unifi: +# +# Requires: curl + jq (apt install jq). set -euo pipefail if [ -z "${1:-}" ]; then - echo "usage: UNIFI_USER=admin UNIFI_PASS=… $(basename "$0") " >&2 + cat <<'EOF' >&2 +usage: + UNIFI_API_KEY=... scripts/discover-unifi.sh + +env: + UNIFI_API_KEY required (Bearer key from Ubiquiti console) + UNIFI_API_BASE optional, default: https://api.ui.com +EOF exit 2 fi -CONTROLLER="$1" -PORT="${UNIFI_PORT:-443}" -SITE="${UNIFI_SITE:-default}" -BASE="https://${CONTROLLER}:${PORT}" +: "${UNIFI_API_KEY:?UNIFI_API_KEY env var required — see ui.com console → API keys}" -: "${UNIFI_USER:?UNIFI_USER env var required}" -: "${UNIFI_PASS:?UNIFI_PASS env var required (never put this in shell history — use a password-manager integration)}" - -COOKIE_JAR=$(mktemp) -trap 'rm -f "$COOKIE_JAR"' EXIT - -login() { - local path="$1" - curl -sk -c "$COOKIE_JAR" -X POST "$BASE$path" \ - -H 'Content-Type: application/json' \ - -d "{\"username\":\"$UNIFI_USER\",\"password\":\"$UNIFI_PASS\"}" \ - -o /dev/null -w '%{http_code}' -} - -# Try UniFi OS endpoint first, fall back to legacy -CODE=$(login /api/auth/login) -if [ "$CODE" = "200" ]; then - STATS_PATH="/proxy/network/api/s/${SITE}/stat/sta" -else - CODE=$(login /api/login) - if [ "$CODE" = "200" ]; then - STATS_PATH="/api/s/${SITE}/stat/sta" - else - echo "error: UniFi login failed (tried /api/auth/login and /api/login, got HTTP $CODE)" >&2 - exit 1 - fi -fi - -json=$(curl -sk -b "$COOKIE_JAR" "$BASE$STATS_PATH") +BASE="${UNIFI_API_BASE:-https://api.ui.com}" +MODE="$1" if ! command -v jq >/dev/null 2>&1; then echo "error: jq not installed (apt install jq)" >&2 exit 2 fi -# Standardize to TSV. Each client record has: ip, mac, hostname, name (alias), ap_mac. -jq -r --arg src "unifi:${CONTROLLER}" ' - .data[] | - [ - (.ip // "-"), - (.mac // "-"), - (.hostname // .name // "-"), - (.ap_mac // "-"), - $src - ] | @tsv -' <<<"$json" +# ---------------------------------------------------------------------- +# Helpers +# ---------------------------------------------------------------------- + +# Paginated GET against the Site Manager API. Concatenates all pages and +# emits each element of the `data` array as one compact JSON object on +# stdout (one per line). Fails loudly on HTTP != 200 (prints the body). +ui_get() { + local path="$1" + local next="" + local url body code + + while : ; do + url="${BASE}${path}" + if [ -n "$next" ]; then + case "$path" in + *\?*) url="${url}&nextToken=${next}" ;; + *) url="${url}?nextToken=${next}" ;; + esac + fi + + tmp=$(mktemp) + code=$(curl -sS \ + -H "X-API-KEY: ${UNIFI_API_KEY}" \ + -H "Accept: application/json" \ + -o "$tmp" -w '%{http_code}' \ + "$url") + + body=$(cat "$tmp"); rm -f "$tmp" + + if [ "$code" != "200" ]; then + echo "error: ${path} returned HTTP ${code}" >&2 + echo "body:" >&2 + echo "$body" | head -20 >&2 + return 1 + fi + + # Emit each data item on its own line + jq -c '.data[]?' <<<"$body" + + # Check for pagination + next=$(jq -r '.nextToken // empty' <<<"$body") + [ -z "$next" ] && break + done +} + +# ---------------------------------------------------------------------- +# Endpoint-specific TSV formatters +# ---------------------------------------------------------------------- + +emit_hosts() { + ui_get /ea/hosts | jq -r ' + [ + (.id // "-"), + (.hardware.name // .userData.name // .reportedState.name // "-"), + (.ipAddress // .reportedState.ip // "-"), + (.hardware.shortname // .type // "-"), + (.reportedState.version // "-"), + "unifi:hosts" + ] | @tsv + ' +} + +emit_sites() { + ui_get /ea/sites | jq -r ' + [ + (.siteId // .id // "-"), + (.meta.name // .name // "-"), + (.hostId // "-"), + (.meta.desc // "-"), + "unifi:sites" + ] | @tsv + ' +} + +emit_devices() { + ui_get /ea/devices | jq -r ' + [ + (.mac // "-"), + (.ip // "-"), + (.name // .hardware.name // "-"), + (.model // .hardware.shortname // "-"), + (.siteId // "-"), + (.hostId // "-"), + "unifi:devices" + ] | @tsv + ' +} + +# ---------------------------------------------------------------------- +# Dispatch +# ---------------------------------------------------------------------- + +case "$MODE" in + hosts) emit_hosts ;; + sites) emit_sites ;; + devices) emit_devices ;; + all) + echo "# === UniFi hosts ===" >&2 + emit_hosts + echo "# === UniFi sites ===" >&2 + emit_sites + echo "# === UniFi devices ===" >&2 + emit_devices + ;; + *) + echo "error: unknown mode '$MODE' — expected hosts|sites|devices|all" >&2 + exit 2 + ;; +esac