118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
"""Nodes router — discover and inspect mesh nodes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, HTTPException, Request, status
|
|
|
|
from castle_api.config import get_registry, settings
|
|
from castle_api.mesh import mesh_state
|
|
from castle_api.models import ComponentSummary, MeshStatus, NodeDetail, NodeSummary
|
|
|
|
router = APIRouter(tags=["nodes"])
|
|
|
|
|
|
def _local_node_summary(registry: object) -> NodeSummary:
|
|
"""Build a NodeSummary for the local node from the registry."""
|
|
return NodeSummary(
|
|
hostname=registry.node.hostname,
|
|
gateway_port=registry.node.gateway_port,
|
|
deployed_count=len(registry.deployed),
|
|
service_count=sum(1 for d in registry.deployed.values() if d.port is not None),
|
|
is_local=True,
|
|
online=True,
|
|
is_stale=False,
|
|
)
|
|
|
|
|
|
def _remote_node_summary(hostname: str, remote: object) -> NodeSummary:
|
|
"""Build a NodeSummary from a RemoteNode."""
|
|
reg = remote.registry
|
|
return NodeSummary(
|
|
hostname=hostname,
|
|
gateway_port=reg.node.gateway_port,
|
|
deployed_count=len(reg.deployed),
|
|
service_count=sum(1 for d in reg.deployed.values() if d.port is not None),
|
|
is_local=False,
|
|
online=remote.online,
|
|
is_stale=remote.is_stale,
|
|
last_seen=remote.last_seen,
|
|
)
|
|
|
|
|
|
def _deployed_to_summaries(registry: object, hostname: str) -> list[ComponentSummary]:
|
|
"""Convert deployed components from a registry into ComponentSummary list."""
|
|
summaries = []
|
|
for name, d in registry.deployed.items():
|
|
summaries.append(
|
|
ComponentSummary(
|
|
id=name,
|
|
category="job" if d.schedule else "service",
|
|
description=d.description,
|
|
behavior=d.behavior,
|
|
stack=d.stack,
|
|
runner=d.runner,
|
|
port=d.port,
|
|
health_path=d.health_path,
|
|
proxy_path=d.proxy_path,
|
|
managed=d.managed,
|
|
schedule=d.schedule,
|
|
node=hostname,
|
|
)
|
|
)
|
|
return summaries
|
|
|
|
|
|
@router.get("/mesh/status", response_model=MeshStatus)
|
|
def get_mesh_status(request: Request) -> MeshStatus:
|
|
"""Get the current state of the mesh coordination layer."""
|
|
mqtt_client = getattr(request.app.state, "mqtt_client", None)
|
|
mdns = getattr(request.app.state, "mdns", None)
|
|
|
|
peers = list(mesh_state.all_nodes(include_stale=True).keys())
|
|
|
|
return MeshStatus(
|
|
enabled=settings.mqtt_enabled,
|
|
mqtt_connected=mqtt_client.connected if mqtt_client else False,
|
|
mqtt_broker_host=mqtt_client.broker_host if mqtt_client else None,
|
|
mqtt_broker_port=mqtt_client.broker_port if mqtt_client else None,
|
|
mdns_enabled=settings.mdns_enabled,
|
|
peer_count=len(peers),
|
|
peers=peers,
|
|
)
|
|
|
|
|
|
@router.get("/nodes", response_model=list[NodeSummary])
|
|
def list_nodes() -> list[NodeSummary]:
|
|
"""List all known nodes (local + discovered remote)."""
|
|
registry = get_registry()
|
|
nodes = [_local_node_summary(registry)]
|
|
|
|
for hostname, remote in mesh_state.all_nodes(include_stale=True).items():
|
|
nodes.append(_remote_node_summary(hostname, remote))
|
|
|
|
return nodes
|
|
|
|
|
|
@router.get("/nodes/{hostname}", response_model=NodeDetail)
|
|
def get_node(hostname: str) -> NodeDetail:
|
|
"""Get detailed info for a specific node."""
|
|
registry = get_registry()
|
|
|
|
# Local node
|
|
if hostname == registry.node.hostname:
|
|
summary = _local_node_summary(registry)
|
|
deployed = _deployed_to_summaries(registry, hostname)
|
|
return NodeDetail(**summary.model_dump(), deployed=deployed)
|
|
|
|
# Remote node
|
|
remote = mesh_state.get_node(hostname)
|
|
if remote is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Node '{hostname}' not found",
|
|
)
|
|
|
|
summary = _remote_node_summary(hostname, remote)
|
|
deployed = _deployed_to_summaries(remote.registry, hostname)
|
|
return NodeDetail(**summary.model_dump(), deployed=deployed)
|