Adds a fleet_deckies table so DB-only consumers (orchestrator, web dashboard, REST API) can see unihost / MACVLAN / IPVLAN deckies without reading the JSON state file. Mirrors DeckyShard field-for-field. Composite PK (host_uuid, name) future-proofs for a mothership that runs both a local fleet and acts as a swarm master. host_uuid defaults to the "local" sentinel — no FK to swarm_hosts because the local mothership isn't enrolled as a worker. Repo additions: upsert_fleet_decky, delete_fleet_decky, list_fleet_deckies, list_running_fleet_deckies, update_fleet_decky_state, plus list_running_deckies which unions topology + fleet + shard sources for the orchestrator. Smoke-tested round-trip against MySQL: upsert, list_running, union view (source="fleet"), delete.
73 lines
3.2 KiB
Python
73 lines
3.2 KiB
Python
"""Fleet decky table — DB mirror of ``decnet-state.json``.
|
|
|
|
The legacy unihost / MACVLAN / IPVLAN deploy path persists fleet state to a
|
|
JSON file (``/var/lib/decnet/decnet-state.json``) via
|
|
:func:`decnet.config.save_state`. That file is consumed directly by
|
|
``decnet status``/``decnet teardown``, the sniffer, and the collector — all
|
|
host-local CLI / worker code that may run on a box without the API daemon.
|
|
|
|
The FleetDecky table is a *mirror* of that JSON state inside MySQL/SQLite so
|
|
DB-only consumers (the orchestrator, the web dashboard, the REST API) can
|
|
see fleet decoys without touching the filesystem.
|
|
|
|
Both writers — CLI ``decnet deploy`` (``engine.deployer.deploy``) and the
|
|
web/API deploy path (``web.router.fleet.api_deploy_deckies``) — write to
|
|
*both* surfaces. A reconciler (``decnet.fleet.reconciler``) handles drift.
|
|
|
|
Schema mirrors :class:`decnet.web.db.models.swarm.DeckyShard` field-for-field
|
|
so the dashboard can render fleet rows with the same card shape. The PK is
|
|
composite ``(host_uuid, name)`` to future-proof for multi-host motherships
|
|
(a master that runs its own local fleet AND swarm-shards onto workers). In
|
|
unihost mode ``host_uuid`` defaults to the sentinel
|
|
:data:`LOCAL_HOST_SENTINEL`; we deliberately do NOT FK to ``swarm_hosts``
|
|
because the local mothership is not enrolled as a swarm worker.
|
|
"""
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from sqlalchemy import Column, Text
|
|
from sqlmodel import Field, SQLModel
|
|
|
|
from ._base import _BIG_TEXT
|
|
|
|
|
|
LOCAL_HOST_SENTINEL = "local"
|
|
|
|
|
|
class FleetDecky(SQLModel, table=True):
|
|
"""A unihost / MACVLAN / IPVLAN decky deployed on the local mothership.
|
|
|
|
Disjoint from :class:`DeckyShard` (SWARM-only) and :class:`TopologyDecky`
|
|
(MazeNET-only). Composite PK lets multiple hosts coexist when a future
|
|
mothership runs both a local fleet and acts as a swarm master.
|
|
"""
|
|
__tablename__ = "fleet_deckies"
|
|
|
|
host_uuid: str = Field(
|
|
default=LOCAL_HOST_SENTINEL, primary_key=True, index=True,
|
|
)
|
|
name: str = Field(primary_key=True)
|
|
# JSON list of service names on this decky (snapshot of assignment).
|
|
services: str = Field(
|
|
sa_column=Column("services", _BIG_TEXT, nullable=False, default="[]")
|
|
)
|
|
# Full serialised DeckyConfig — lets the dashboard render the same rich
|
|
# card (hostname/distro/archetype/service_config/mutate_interval) without
|
|
# round-tripping to load_state() on every page render.
|
|
decky_config: Optional[str] = Field(
|
|
default=None, sa_column=Column("decky_config", _BIG_TEXT, nullable=True)
|
|
)
|
|
decky_ip: Optional[str] = Field(default=None)
|
|
# pending|running|failed|torn_down|degraded|tearing_down|teardown_failed
|
|
state: str = Field(default="pending", index=True)
|
|
last_error: Optional[str] = Field(
|
|
default=None, sa_column=Column("last_error", Text, nullable=True),
|
|
)
|
|
compose_hash: Optional[str] = Field(default=None)
|
|
# Last reconciler observation (docker inspect) — lets the dashboard show
|
|
# "stale" rows whose reconciler hasn't ticked.
|
|
last_seen: Optional[datetime] = Field(default=None)
|
|
updated_at: datetime = Field(
|
|
default_factory=lambda: datetime.now(timezone.utc)
|
|
)
|