a7a281cb11
First-party plugins (host_agent, http, snmp, traefik, unifi, docker) are now tracked under plugins/ and baked into the image, so they version atomically with core — ending the cross-repo import drift the roundtable->steward rename exposed. History for these files is preserved in the archived Roundtable-plugins repo. Plugin discovery becomes multi-root: PLUGIN_DIR (single) -> PLUGIN_DIRS (bundled first, then external) + PLUGIN_INSTALL_DIR. Bundled ships in the image; third-party plugins still mount at runtime into the external root (STEWARD_PLUGIN_DIR, default /data/plugins) and downloads/installs land there. Bundled shadows external on a name collision. - config.py: load_bootstrap returns plugin_dirs + plugin_install_dir - app.py: iterate PLUGIN_DIRS at the migration + load sites - migration_runner.py: discover_all_in() unions every plugin root - plugin_manager.py: resolve_plugin_path() (pure, first-root-wins); load / install / hot-reload span all roots; installs target the external root - settings/routes.py: _discover_plugins scans all roots, dedup bundled-first - Dockerfile: COPY plugins/ ; docker-compose: drop host bind, document external - tests/test_plugin_dirs.py: resolution, multi-root discovery, bootstrap split Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
183 lines
10 KiB
Python
183 lines
10 KiB
Python
# plugins/unifi/models.py
|
|
from __future__ import annotations
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from sqlalchemy import Boolean, DateTime, Float, Integer, String, Text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
from fabledscryer.models.base import Base
|
|
|
|
|
|
# ── Time-series (one row per scrape) ──────────────────────────────────────────
|
|
|
|
class UnifiWanStat(Base):
|
|
"""WAN interface health — one row per scrape."""
|
|
__tablename__ = "unifi_wan_stats"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
is_up: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
wan_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
latency_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
rx_bytes_rate: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
tx_bytes_rate: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
uptime_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
|
class UnifiClientSnapshot(Base):
|
|
"""Aggregated client counts + top talkers — one row per scrape."""
|
|
__tablename__ = "unifi_client_snapshots"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
total_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
wireless_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
wired_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
guest_clients: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
top_clients_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
|
|
# JSON: [{"mac","hostname","ip","type","tx_rate","rx_rate","signal","essid"}]
|
|
|
|
|
|
class UnifiWlanStat(Base):
|
|
"""Per-SSID/radio stats — one row per scrape per SSID+radio combination."""
|
|
__tablename__ = "unifi_wlan_stats"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
ssid: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
radio: Mapped[str | None] = mapped_column(String(8), nullable=True) # ng=2.4GHz, na=5GHz, 6e=6GHz
|
|
num_sta: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
tx_bytes: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
rx_bytes: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
channel: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
satisfaction: Mapped[int | None] = mapped_column(Integer, nullable=True) # 0-100
|
|
|
|
|
|
class UnifiDpiSnapshot(Base):
|
|
"""DPI application/category traffic snapshot — one row per scrape (JSON blob)."""
|
|
__tablename__ = "unifi_dpi_snapshots"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
# JSON: [{"cat_id", "cat_name", "rx_bytes", "tx_bytes"}] aggregated across all clients
|
|
by_category_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
|
|
# JSON: [{"mac", "hostname", "top_cats": [{"cat_name", "bytes"}]}]
|
|
by_client_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]")
|
|
|
|
|
|
# ── Upserted inventory (keyed by UniFi ID or MAC) ─────────────────────────────
|
|
|
|
class UnifiDevice(Base):
|
|
"""Network infrastructure devices — upserted by MAC each scrape."""
|
|
__tablename__ = "unifi_devices"
|
|
|
|
mac: Mapped[str] = mapped_column(String(32), primary_key=True)
|
|
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
model: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
device_type: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
|
ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
state: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
uptime_seconds: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
version: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiKnownClient(Base):
|
|
"""All historically seen network clients — upserted by MAC."""
|
|
__tablename__ = "unifi_known_clients"
|
|
|
|
mac: Mapped[str] = mapped_column(String(32), primary_key=True)
|
|
hostname: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
name: Mapped[str | None] = mapped_column(String(255), nullable=True) # user-set alias
|
|
ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
mac_vendor: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
is_blocked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
first_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
last_seen: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiEvent(Base):
|
|
"""Site event log — upserted by UniFi event ID to avoid duplicates."""
|
|
__tablename__ = "unifi_events"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
event_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
event_key: Mapped[str] = mapped_column(String(64), nullable=False) # e.g. EVT_WU_Connected
|
|
category: Mapped[str] = mapped_column(String(16), nullable=False) # wlan, wired, ap, ids, wan, system
|
|
message: Mapped[str] = mapped_column(Text, nullable=False)
|
|
source_mac: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
|
source_ip: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
details_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
|
|
|
|
|
|
class UnifiAlarm(Base):
|
|
"""Active alarms — upserted by UniFi alarm ID."""
|
|
__tablename__ = "unifi_alarms"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
alarm_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
alarm_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
message: Mapped[str] = mapped_column(Text, nullable=False)
|
|
archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiSpeedtestResult(Base):
|
|
"""WAN speed test results — upserted by run timestamp."""
|
|
__tablename__ = "unifi_speedtest_results"
|
|
|
|
run_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), primary_key=True)
|
|
download_mbps: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
upload_mbps: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
latency_ms: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
server_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
|
|
|
|
# ── Config inventory (upserted by UniFi ID) ───────────────────────────────────
|
|
|
|
class UnifiNetwork(Base):
|
|
"""VLAN/network configurations — upserted by UniFi ID."""
|
|
__tablename__ = "unifi_networks"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
purpose: Mapped[str | None] = mapped_column(String(32), nullable=True) # corporate, guest, vlan-only
|
|
vlan: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
ip_subnet: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
dhcp_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
is_guest: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiPortForward(Base):
|
|
"""Port forwarding rules — upserted by UniFi ID."""
|
|
__tablename__ = "unifi_port_forwards"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
proto: Mapped[str] = mapped_column(String(8), nullable=False, default="tcp")
|
|
dst_port: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
fwd_ip: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
fwd_port: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
src_filter: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class UnifiFwRule(Base):
|
|
"""Firewall rules — upserted by UniFi ID."""
|
|
__tablename__ = "unifi_fw_rules"
|
|
|
|
unifi_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
ruleset: Mapped[str] = mapped_column(String(16), nullable=False) # WAN_IN, LAN_IN, etc.
|
|
rule_index: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
action: Mapped[str] = mapped_column(String(16), nullable=False) # accept, drop, reject
|
|
protocol: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
|
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
src_address: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
dst_address: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
scraped_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|