6e9465942a
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
110 lines
3.6 KiB
Python
110 lines
3.6 KiB
Python
"""Group CRUD and bulk action endpoints for the Farm Manager API server."""
|
|
|
|
import asyncio
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from server.config import get_node_url
|
|
from server.group_store import GroupStore
|
|
from server.models import Group
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _get_store() -> GroupStore:
|
|
return GroupStore()
|
|
|
|
|
|
def _require_group(group_id: str) -> Group:
|
|
"""Return the group or raise 404."""
|
|
group = _get_store().get_group(group_id)
|
|
if group is None:
|
|
raise HTTPException(status_code=404, detail=f"Group not found: {group_id}")
|
|
return group
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CRUD
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/api/groups")
|
|
def list_groups():
|
|
"""Return all groups."""
|
|
return [g.model_dump() for g in _get_store().list_groups()]
|
|
|
|
|
|
@router.post("/api/groups")
|
|
def create_group(group: Group):
|
|
"""Create a new group. Returns 409 if duplicate id."""
|
|
try:
|
|
created = _get_store().create_group(group)
|
|
return created.model_dump()
|
|
except ValueError:
|
|
raise HTTPException(status_code=409, detail=f"Group '{group.id}' already exists")
|
|
|
|
|
|
@router.put("/api/groups/{group_id}")
|
|
def update_group(group_id: str, group: Group):
|
|
"""Update an existing group by id. Returns 404 if not found."""
|
|
updated = _get_store().update_group(group_id, group)
|
|
if updated is None:
|
|
raise HTTPException(status_code=404, detail=f"Group not found: {group_id}")
|
|
return updated.model_dump()
|
|
|
|
|
|
@router.delete("/api/groups/{group_id}")
|
|
def delete_group(group_id: str):
|
|
"""Delete a group by id. Returns 404 if not found."""
|
|
if not _get_store().delete_group(group_id):
|
|
raise HTTPException(status_code=404, detail=f"Group not found: {group_id}")
|
|
return {"success": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bulk actions — start/stop/restart all services in a group
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _send_action(
|
|
http_client: httpx.AsyncClient, node: str, container: str, action: str
|
|
) -> dict:
|
|
"""Send a start/stop/restart action to a single container via its node agent."""
|
|
url = get_node_url(node)
|
|
if url is None:
|
|
return {"node": node, "container": container, "success": False, "error": f"Unknown node: {node}"}
|
|
try:
|
|
resp = await http_client.post(f"{url}/containers/{container}/{action}", timeout=30.0)
|
|
return {"node": node, "container": container, **resp.json()}
|
|
except Exception as exc:
|
|
return {"node": node, "container": container, "success": False, "error": str(exc)}
|
|
|
|
|
|
async def _bulk_action(group_id: str, action: str) -> list[dict]:
|
|
"""Execute *action* on every service in the group."""
|
|
group = _require_group(group_id)
|
|
async with httpx.AsyncClient() as client:
|
|
tasks = [
|
|
_send_action(client, svc.node, svc.container, action)
|
|
for svc in group.services
|
|
]
|
|
results = await asyncio.gather(*tasks)
|
|
return list(results)
|
|
|
|
|
|
@router.post("/api/groups/{group_id}/start")
|
|
async def start_group(group_id: str):
|
|
"""Start all services in a group."""
|
|
return await _bulk_action(group_id, "start")
|
|
|
|
|
|
@router.post("/api/groups/{group_id}/stop")
|
|
async def stop_group(group_id: str):
|
|
"""Stop all services in a group."""
|
|
return await _bulk_action(group_id, "stop")
|
|
|
|
|
|
@router.post("/api/groups/{group_id}/restart")
|
|
async def restart_group(group_id: str):
|
|
"""Restart all services in a group."""
|
|
return await _bulk_action(group_id, "restart")
|