92 lines
3.8 KiB
Python
92 lines
3.8 KiB
Python
"""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)
|