feat(plugins): fold first-party plugins in-tree; bundled + external roots
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>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# 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))
|
||||
Reference in New Issue
Block a user