scripts/discover-unifi: switch to Site Manager API (developer.ui.com)
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).
This commit is contained in:
+147
-59
@@ -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/<site>/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/<site>/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 <controller-host>
|
||||
# UNIFI_API_KEY=<your-key> scripts/discover-unifi.sh hosts
|
||||
# UNIFI_API_KEY=<your-key> scripts/discover-unifi.sh sites
|
||||
# UNIFI_API_KEY=<your-key> scripts/discover-unifi.sh devices
|
||||
# UNIFI_API_KEY=<your-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:<endpoint>
|
||||
#
|
||||
# Requires: curl + jq (apt install jq).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${1:-}" ]; then
|
||||
echo "usage: UNIFI_USER=admin UNIFI_PASS=… $(basename "$0") <controller-host>" >&2
|
||||
cat <<'EOF' >&2
|
||||
usage:
|
||||
UNIFI_API_KEY=... scripts/discover-unifi.sh <hosts|sites|devices|all>
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user