af60ca446d
The fabledscryer->steward rename had only ever reached host_agent. The other five bundled plugins (http, snmp, traefik, unifi, docker) still imported `from fabledscryer.*` (package no longer exists) and read FABLEDSCRYER_* env vars — so every one of them was broken at import since the original rebrand. CI stayed green only because none are enabled by default and migrations don't import plugin modules. Now that they version in-tree, complete the rename: - fabledscryer.* -> steward.* imports across all five plugins - FABLEDSCRYER_* -> STEWARD_* in plugin migration env.py files - author/repository/homepage + user-facing 'Fabled Scryer' strings -> Steward - snmp/scheduler.py: also drop dead `now`/datetime; record_metric from steward Adds tests/test_no_legacy_names.py — fails if 'scryer'/'roundtable' ever reappear in shipped code (the drift bit twice; this stops a third time). Also clears pre-existing ruff lint debt (unused imports, semicolon statements, mid-file import) surfaced by the new lint lane. 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 steward.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))
|