feat(swarm): add decnet swarm deckies to list deployed shards by host
`swarm list` only shows enrolled workers — there was no way to see which deckies are running and where. Adds GET /swarm/deckies on the controller (joins DeckyShard with SwarmHost for name/address/status) plus the CLI wrapper with --host / --state filters and --json.
This commit is contained in:
@@ -494,6 +494,66 @@ def swarm_check(
|
||||
console.print(table)
|
||||
|
||||
|
||||
@swarm_app.command("deckies")
|
||||
def swarm_deckies(
|
||||
host: Optional[str] = typer.Option(None, "--host", help="Filter by worker name or UUID"),
|
||||
state: Optional[str] = typer.Option(None, "--state", help="Filter by shard state (pending|running|failed|torn_down)"),
|
||||
url: Optional[str] = typer.Option(None, "--url", help="Override swarm controller URL"),
|
||||
json_out: bool = typer.Option(False, "--json", help="Emit JSON instead of a table"),
|
||||
) -> None:
|
||||
"""List deployed deckies across the swarm with their owning worker host."""
|
||||
base = _swarmctl_base_url(url)
|
||||
|
||||
host_uuid: Optional[str] = None
|
||||
if host:
|
||||
resp = _http_request("GET", base + "/swarm/hosts")
|
||||
rows = resp.json()
|
||||
match = next((r for r in rows if r.get("uuid") == host or r.get("name") == host), None)
|
||||
if match is None:
|
||||
console.print(f"[red]No enrolled worker matching '{host}'.[/]")
|
||||
raise typer.Exit(1)
|
||||
host_uuid = match["uuid"]
|
||||
|
||||
query = []
|
||||
if host_uuid:
|
||||
query.append(f"host_uuid={host_uuid}")
|
||||
if state:
|
||||
query.append(f"state={state}")
|
||||
path = "/swarm/deckies" + ("?" + "&".join(query) if query else "")
|
||||
|
||||
resp = _http_request("GET", base + path)
|
||||
rows = resp.json()
|
||||
|
||||
if json_out:
|
||||
console.print_json(data=rows)
|
||||
return
|
||||
|
||||
if not rows:
|
||||
console.print("[dim]No deckies deployed.[/]")
|
||||
return
|
||||
|
||||
table = Table(title="DECNET swarm deckies")
|
||||
for col in ("decky", "host", "address", "state", "services"):
|
||||
table.add_column(col)
|
||||
for r in rows:
|
||||
services = ",".join(r.get("services") or []) or "—"
|
||||
state_val = r.get("state") or "pending"
|
||||
colored = {
|
||||
"running": f"[green]{state_val}[/]",
|
||||
"failed": f"[red]{state_val}[/]",
|
||||
"pending": f"[yellow]{state_val}[/]",
|
||||
"torn_down": f"[dim]{state_val}[/]",
|
||||
}.get(state_val, state_val)
|
||||
table.add_row(
|
||||
r.get("decky_name") or "",
|
||||
r.get("host_name") or "<unknown>",
|
||||
r.get("host_address") or "",
|
||||
colored,
|
||||
services,
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@swarm_app.command("decommission")
|
||||
def swarm_decommission(
|
||||
name: Optional[str] = typer.Option(None, "--name", help="Worker hostname"),
|
||||
|
||||
@@ -307,6 +307,20 @@ class SwarmHostView(BaseModel):
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class DeckyShardView(BaseModel):
|
||||
"""One decky → host mapping, enriched with the host's identity for display."""
|
||||
decky_name: str
|
||||
host_uuid: str
|
||||
host_name: str
|
||||
host_address: str
|
||||
host_status: str
|
||||
services: list[str]
|
||||
state: str
|
||||
last_error: Optional[str] = None
|
||||
compose_hash: Optional[str] = None
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class SwarmDeployRequest(BaseModel):
|
||||
config: DecnetConfig
|
||||
dry_run: bool = False
|
||||
|
||||
@@ -15,6 +15,7 @@ from .api_deploy_swarm import router as deploy_swarm_router
|
||||
from .api_teardown_swarm import router as teardown_swarm_router
|
||||
from .api_get_swarm_health import router as get_swarm_health_router
|
||||
from .api_check_hosts import router as check_hosts_router
|
||||
from .api_list_deckies import router as list_deckies_router
|
||||
|
||||
swarm_router = APIRouter(prefix="/swarm")
|
||||
|
||||
@@ -27,6 +28,7 @@ swarm_router.include_router(decommission_host_router)
|
||||
# Deployments
|
||||
swarm_router.include_router(deploy_swarm_router)
|
||||
swarm_router.include_router(teardown_swarm_router)
|
||||
swarm_router.include_router(list_deckies_router)
|
||||
|
||||
# Health
|
||||
swarm_router.include_router(get_swarm_health_router)
|
||||
|
||||
47
decnet/web/router/swarm/api_list_deckies.py
Normal file
47
decnet/web/router/swarm/api_list_deckies.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""GET /swarm/deckies — list decky shards with their worker host's identity.
|
||||
|
||||
The DeckyShard table maps decky_name → host_uuid; users want to see which
|
||||
deckies are running and *where*, so we enrich each shard with the owning
|
||||
host's name/address/status from SwarmHost rather than making callers do
|
||||
the join themselves.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from decnet.web.db.repository import BaseRepository
|
||||
from decnet.web.dependencies import get_repo
|
||||
from decnet.web.db.models import DeckyShardView
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/deckies", response_model=list[DeckyShardView], tags=["Swarm Deckies"])
|
||||
async def api_list_deckies(
|
||||
host_uuid: Optional[str] = None,
|
||||
state: Optional[str] = None,
|
||||
repo: BaseRepository = Depends(get_repo),
|
||||
) -> list[DeckyShardView]:
|
||||
shards = await repo.list_decky_shards(host_uuid)
|
||||
hosts = {h["uuid"]: h for h in await repo.list_swarm_hosts()}
|
||||
|
||||
out: list[DeckyShardView] = []
|
||||
for s in shards:
|
||||
if state and s.get("state") != state:
|
||||
continue
|
||||
host = hosts.get(s["host_uuid"], {})
|
||||
out.append(DeckyShardView(
|
||||
decky_name=s["decky_name"],
|
||||
host_uuid=s["host_uuid"],
|
||||
host_name=host.get("name") or "<unknown>",
|
||||
host_address=host.get("address") or "",
|
||||
host_status=host.get("status") or "unknown",
|
||||
services=s.get("services") or [],
|
||||
state=s.get("state") or "pending",
|
||||
last_error=s.get("last_error"),
|
||||
compose_hash=s.get("compose_hash"),
|
||||
updated_at=s["updated_at"],
|
||||
))
|
||||
return out
|
||||
Reference in New Issue
Block a user