"""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")