refactor(db): extract DeckiesMixin
Moves the 4 decky-shard CRUD methods into sqlmodel_repo/deckies.py.
This commit is contained in:
@@ -67,12 +67,14 @@ from decnet.web.db.sqlmodel_repo._helpers import ( # noqa: F401 (re-exported f
|
|||||||
)
|
)
|
||||||
from decnet.web.db.sqlmodel_repo.attacker_intel import AttackerIntelMixin
|
from decnet.web.db.sqlmodel_repo.attacker_intel import AttackerIntelMixin
|
||||||
from decnet.web.db.sqlmodel_repo.auth import AuthMixin
|
from decnet.web.db.sqlmodel_repo.auth import AuthMixin
|
||||||
|
from decnet.web.db.sqlmodel_repo.deckies import DeckiesMixin
|
||||||
from decnet.web.db.sqlmodel_repo.swarm import SwarmMixin
|
from decnet.web.db.sqlmodel_repo.swarm import SwarmMixin
|
||||||
|
|
||||||
|
|
||||||
class SQLModelRepository(
|
class SQLModelRepository(
|
||||||
AttackerIntelMixin,
|
AttackerIntelMixin,
|
||||||
AuthMixin,
|
AuthMixin,
|
||||||
|
DeckiesMixin,
|
||||||
SwarmMixin,
|
SwarmMixin,
|
||||||
BaseRepository,
|
BaseRepository,
|
||||||
):
|
):
|
||||||
@@ -1721,82 +1723,6 @@ class SQLModelRepository(
|
|||||||
|
|
||||||
# ------------------------------------------------------------- swarm
|
# ------------------------------------------------------------- swarm
|
||||||
|
|
||||||
async def upsert_decky_shard(self, data: dict[str, Any]) -> None:
|
|
||||||
payload = {**data, "updated_at": datetime.now(timezone.utc)}
|
|
||||||
if isinstance(payload.get("services"), list):
|
|
||||||
payload["services"] = orjson.dumps(payload["services"]).decode()
|
|
||||||
async with self._session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
select(DeckyShard).where(DeckyShard.decky_name == payload["decky_name"])
|
|
||||||
)
|
|
||||||
existing = result.scalar_one_or_none()
|
|
||||||
if existing:
|
|
||||||
for k, v in payload.items():
|
|
||||||
setattr(existing, k, v)
|
|
||||||
session.add(existing)
|
|
||||||
else:
|
|
||||||
session.add(DeckyShard(**payload))
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
async def list_decky_shards(
|
|
||||||
self, host_uuid: Optional[str] = None
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
statement = select(DeckyShard).order_by(asc(DeckyShard.decky_name))
|
|
||||||
if host_uuid:
|
|
||||||
statement = statement.where(DeckyShard.host_uuid == host_uuid)
|
|
||||||
async with self._session() as session:
|
|
||||||
result = await session.execute(statement)
|
|
||||||
out: list[dict[str, Any]] = []
|
|
||||||
for r in result.scalars().all():
|
|
||||||
d = r.model_dump(mode="json")
|
|
||||||
raw = d.get("services")
|
|
||||||
if isinstance(raw, str):
|
|
||||||
try:
|
|
||||||
d["services"] = json.loads(raw)
|
|
||||||
except (json.JSONDecodeError, TypeError):
|
|
||||||
d["services"] = []
|
|
||||||
# Flatten the stored DeckyConfig snapshot into the row so
|
|
||||||
# routers can hand it to DeckyShardView without re-parsing.
|
|
||||||
# Rows predating the migration have decky_config=NULL and
|
|
||||||
# fall through with the default (None/{}) view values.
|
|
||||||
cfg_raw = d.get("decky_config")
|
|
||||||
if isinstance(cfg_raw, str):
|
|
||||||
try:
|
|
||||||
cfg = json.loads(cfg_raw)
|
|
||||||
except (json.JSONDecodeError, TypeError):
|
|
||||||
cfg = {}
|
|
||||||
if isinstance(cfg, dict):
|
|
||||||
for k in ("hostname", "distro", "archetype",
|
|
||||||
"service_config", "mutate_interval",
|
|
||||||
"last_mutated"):
|
|
||||||
if k in cfg and d.get(k) is None:
|
|
||||||
d[k] = cfg[k]
|
|
||||||
# Keep decky_ip authoritative from the column (newer
|
|
||||||
# heartbeats overwrite it) but fall back to the
|
|
||||||
# snapshot if the column is still NULL.
|
|
||||||
if not d.get("decky_ip") and cfg.get("ip"):
|
|
||||||
d["decky_ip"] = cfg["ip"]
|
|
||||||
out.append(d)
|
|
||||||
return out
|
|
||||||
|
|
||||||
async def delete_decky_shards_for_host(self, host_uuid: str) -> int:
|
|
||||||
async with self._session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
text("DELETE FROM decky_shards WHERE host_uuid = :u"),
|
|
||||||
{"u": host_uuid},
|
|
||||||
)
|
|
||||||
await session.commit()
|
|
||||||
return result.rowcount or 0
|
|
||||||
|
|
||||||
async def delete_decky_shard(self, decky_name: str) -> bool:
|
|
||||||
async with self._session() as session:
|
|
||||||
result = await session.execute(
|
|
||||||
text("DELETE FROM decky_shards WHERE decky_name = :n"),
|
|
||||||
{"n": decky_name},
|
|
||||||
)
|
|
||||||
await session.commit()
|
|
||||||
return bool(result.rowcount)
|
|
||||||
|
|
||||||
# -------------------------------------------------------------- fleet
|
# -------------------------------------------------------------- fleet
|
||||||
|
|
||||||
async def upsert_fleet_decky(self, data: dict[str, Any]) -> None:
|
async def upsert_fleet_decky(self, data: dict[str, Any]) -> None:
|
||||||
|
|||||||
91
decnet/web/db/sqlmodel_repo/deckies.py
Normal file
91
decnet/web/db/sqlmodel_repo/deckies.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
"""Decky-shard CRUD (per-host shard registrations)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
from sqlalchemy import asc, select, text
|
||||||
|
|
||||||
|
from decnet.web.db.models import DeckyShard
|
||||||
|
|
||||||
|
|
||||||
|
class DeckiesMixin:
|
||||||
|
"""Mixin: composed onto ``SQLModelRepository``."""
|
||||||
|
|
||||||
|
async def upsert_decky_shard(self, data: dict[str, Any]) -> None:
|
||||||
|
payload = {**data, "updated_at": datetime.now(timezone.utc)}
|
||||||
|
if isinstance(payload.get("services"), list):
|
||||||
|
payload["services"] = orjson.dumps(payload["services"]).decode()
|
||||||
|
async with self._session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(DeckyShard).where(DeckyShard.decky_name == payload["decky_name"])
|
||||||
|
)
|
||||||
|
existing = result.scalar_one_or_none()
|
||||||
|
if existing:
|
||||||
|
for k, v in payload.items():
|
||||||
|
setattr(existing, k, v)
|
||||||
|
session.add(existing)
|
||||||
|
else:
|
||||||
|
session.add(DeckyShard(**payload))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
async def list_decky_shards(
|
||||||
|
self, host_uuid: Optional[str] = None
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
statement = select(DeckyShard).order_by(asc(DeckyShard.decky_name))
|
||||||
|
if host_uuid:
|
||||||
|
statement = statement.where(DeckyShard.host_uuid == host_uuid)
|
||||||
|
async with self._session() as session:
|
||||||
|
result = await session.execute(statement)
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for r in result.scalars().all():
|
||||||
|
d = r.model_dump(mode="json")
|
||||||
|
raw = d.get("services")
|
||||||
|
if isinstance(raw, str):
|
||||||
|
try:
|
||||||
|
d["services"] = json.loads(raw)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
d["services"] = []
|
||||||
|
# Flatten the stored DeckyConfig snapshot into the row so
|
||||||
|
# routers can hand it to DeckyShardView without re-parsing.
|
||||||
|
# Rows predating the migration have decky_config=NULL and
|
||||||
|
# fall through with the default (None/{}) view values.
|
||||||
|
cfg_raw = d.get("decky_config")
|
||||||
|
if isinstance(cfg_raw, str):
|
||||||
|
try:
|
||||||
|
cfg = json.loads(cfg_raw)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
cfg = {}
|
||||||
|
if isinstance(cfg, dict):
|
||||||
|
for k in ("hostname", "distro", "archetype",
|
||||||
|
"service_config", "mutate_interval",
|
||||||
|
"last_mutated"):
|
||||||
|
if k in cfg and d.get(k) is None:
|
||||||
|
d[k] = cfg[k]
|
||||||
|
# Keep decky_ip authoritative from the column (newer
|
||||||
|
# heartbeats overwrite it) but fall back to the
|
||||||
|
# snapshot if the column is still NULL.
|
||||||
|
if not d.get("decky_ip") and cfg.get("ip"):
|
||||||
|
d["decky_ip"] = cfg["ip"]
|
||||||
|
out.append(d)
|
||||||
|
return out
|
||||||
|
|
||||||
|
async def delete_decky_shards_for_host(self, host_uuid: str) -> int:
|
||||||
|
async with self._session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
text("DELETE FROM decky_shards WHERE host_uuid = :u"),
|
||||||
|
{"u": host_uuid},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount or 0
|
||||||
|
|
||||||
|
async def delete_decky_shard(self, decky_name: str) -> bool:
|
||||||
|
async with self._session() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
text("DELETE FROM decky_shards WHERE decky_name = :n"),
|
||||||
|
{"n": decky_name},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return bool(result.rowcount)
|
||||||
Reference in New Issue
Block a user