Refresh RackPeek and UniFi network inventory

This commit is contained in:
Fizzlepoof
2026-05-21 23:16:11 +00:00
parent 92f88178e7
commit ee9a2cfcee
5 changed files with 475 additions and 1 deletions

View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""Minimal Doris helper for UniFi OS / Network inventory reads.
Reads credentials from automation/.env by default and performs a cookie-based
login against UniFi OS, then calls Network app endpoints through
`/proxy/network/api/s/<site>/...`.
Default action prints a compact JSON summary suitable for RackPeek refresh work.
No secrets are printed.
"""
from __future__ import annotations
import argparse
import json
import os
import ssl
import sys
from http.cookiejar import CookieJar
from pathlib import Path
from typing import Any
from urllib import error, request
DEFAULT_TIMEOUT = 20
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent.parent
DEFAULT_ENV_PATH = REPO_ROOT / 'automation' / '.env'
def load_env_file(path: Path) -> None:
if not path.exists():
return
for raw_line in path.read_text(encoding='utf-8').splitlines():
line = raw_line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
key, value = line.split('=', 1)
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
def env_bool(name: str, default: bool) -> bool:
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in {'1', 'true', 'yes', 'on'}
class UniFiClient:
def __init__(self, base_url: str, username: str, password: str, *, verify_tls: bool = True, timeout: int = DEFAULT_TIMEOUT) -> None:
self.base_url = base_url.rstrip('/')
self.username = username
self.password = password
self.timeout = timeout
self.cookie_jar = CookieJar()
handlers: list[Any] = [request.HTTPCookieProcessor(self.cookie_jar)]
if not verify_tls:
handlers.append(request.HTTPSHandler(context=ssl._create_unverified_context()))
self.opener = request.build_opener(*handlers)
def _json_request(self, method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, Any]:
url = self.base_url + path
data = None
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'doris-unifi-helper/1.0',
}
if body is not None:
data = json.dumps(body).encode('utf-8')
req = request.Request(url, data=data, method=method.upper(), headers=headers)
try:
with self.opener.open(req, timeout=self.timeout) as resp:
raw = resp.read().decode('utf-8', 'replace')
ctype = resp.headers.get('Content-Type', '')
payload = json.loads(raw) if 'json' in ctype else raw
return resp.status, payload
except error.HTTPError as exc:
raw = exc.read().decode('utf-8', 'replace')
try:
payload = json.loads(raw)
except Exception:
payload = raw
return exc.code, payload
def login(self) -> None:
status, payload = self._json_request('POST', '/api/auth/login', {
'username': self.username,
'password': self.password,
'rememberMe': True,
})
if status >= 300:
raise SystemExit(f'UniFi login failed: HTTP {status} {payload!r}')
def get(self, path: str) -> Any:
status, payload = self._json_request('GET', path)
if status >= 300:
raise SystemExit(f'UniFi GET {path} failed: HTTP {status} {payload!r}')
return payload
def site_path(self, site: str, suffix: str) -> str:
suffix = suffix.lstrip('/')
return f'/proxy/network/api/s/{site}/{suffix}'
def response_data(payload: Any) -> Any:
if isinstance(payload, dict) and 'data' in payload:
return payload['data']
return payload
def device_summary(devices: list[dict[str, Any]]) -> list[dict[str, Any]]:
out = []
for dev in devices:
out.append({
'name': dev.get('name') or dev.get('hostname') or dev.get('mac'),
'model': dev.get('model'),
'type': dev.get('type') or dev.get('device_type'),
'state': dev.get('state'),
'ip': dev.get('ip'),
'mac': dev.get('mac'),
'version': dev.get('version'),
'uplink': ((dev.get('uplink') or {}).get('name') if isinstance(dev.get('uplink'), dict) else None),
})
return out
def client_summary(clients: list[dict[str, Any]]) -> list[dict[str, Any]]:
out = []
for client in clients:
out.append({
'name': client.get('name') or client.get('hostname') or client.get('mac'),
'ip': client.get('ip'),
'mac': client.get('mac'),
'network': client.get('network'),
'ap_mac': client.get('ap_mac'),
'oui': client.get('oui'),
})
return out
def build_summary(client: UniFiClient, site: str) -> dict[str, Any]:
devices = response_data(client.get(client.site_path(site, 'stat/device')))
health = response_data(client.get(client.site_path(site, 'stat/health')))
clients = response_data(client.get(client.site_path(site, 'stat/sta')))
return {
'site': site,
'device_count': len(devices),
'client_count': len(clients),
'devices': device_summary(devices),
'health': health,
'clients': client_summary(clients),
}
def main() -> int:
parser = argparse.ArgumentParser(description='Read UniFi Network inventory for Doris.')
parser.add_argument('--env-file', default=str(DEFAULT_ENV_PATH), help='Path to .env containing UNIFI_* vars')
parser.add_argument('--site', help='UniFi site name; defaults to UNIFI_SITE or default')
parser.add_argument('--raw', action='store_true', help='Print raw device/client/health payloads instead of compact summary')
parser.add_argument('--timeout', type=int, default=DEFAULT_TIMEOUT)
args = parser.parse_args()
load_env_file(Path(args.env_file))
base_url = os.environ.get('UNIFI_BASE_URL', 'https://10.5.0.1')
username = os.environ.get('UNIFI_USERNAME')
password = os.environ.get('UNIFI_PASSWORD')
site = args.site or os.environ.get('UNIFI_SITE', 'default')
verify_tls = env_bool('UNIFI_VERIFY_TLS', False)
if not username or not password:
raise SystemExit('Missing UNIFI_USERNAME or UNIFI_PASSWORD in env file/environment.')
client = UniFiClient(base_url, username, password, verify_tls=verify_tls, timeout=args.timeout)
client.login()
if args.raw:
payload = {
'devices': response_data(client.get(client.site_path(site, 'stat/device'))),
'health': response_data(client.get(client.site_path(site, 'stat/health'))),
'clients': response_data(client.get(client.site_path(site, 'stat/sta'))),
}
else:
payload = build_summary(client, site)
print(json.dumps(payload, indent=2, sort_keys=True))
return 0
if __name__ == '__main__':
raise SystemExit(main())