# SPDX-License-Identifier: AGPL-3.0-or-later """POST /topologies — generate and persist a new MazeNET topology.""" from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.exc import IntegrityError from decnet.telemetry import traced as _traced from decnet.topology.allocator import reserved_subnets from decnet.topology.config import TopologyConfig from decnet.topology.generator import generate from decnet.topology.persistence import persist from decnet.web.db.models import TopologyGenerateRequest, TopologySummary from decnet.web.dependencies import repo, require_admin from decnet.web.router.topology._target_host import validate_target_host router = APIRouter() @router.post( "/", tags=["MazeNET Topologies"], response_model=TopologySummary, status_code=status.HTTP_201_CREATED, responses={ 400: {"description": "Malformed or invalid generation parameters"}, 401: {"description": "Missing or invalid credentials"}, 403: {"description": "Insufficient permissions"}, 409: {"description": "Duplicate topology name, or generator could not allocate subnets (exhausted pool)"}, }, ) @_traced("api.topology.create") async def api_create_topology( body: TopologyGenerateRequest, _admin: dict = Depends(require_admin), ) -> TopologySummary: await validate_target_host(repo, body.mode, body.target_host_uuid) try: config = TopologyConfig( name=body.name, mode=body.mode, depth=body.depth, branching_factor=body.branching_factor, deckies_per_lan_min=body.deckies_per_lan_min, deckies_per_lan_max=body.deckies_per_lan_max, bridge_forward_probability=body.bridge_forward_probability, cross_edge_probability=body.cross_edge_probability, services_explicit=body.services_explicit, randomize_services=body.randomize_services, seed=body.seed, ) except (ValueError, TypeError) as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc try: plan = generate(config, reserved_subnets=await reserved_subnets(repo)) except RuntimeError as exc: # Subnet allocator exhaustion or similar planner-level failure. raise HTTPException(status_code=409, detail=str(exc)) from exc except (ValueError, TypeError) as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc try: topology_id = await persist(repo, plan, target_host_uuid=body.target_host_uuid) except IntegrityError as exc: # Unique constraint on topologies.name is the only integrity # error the create path can realistically hit — inspecting the # constraint name keeps us from silently mapping unrelated # integrity failures to 409. msg = str(exc.orig) if exc.orig is not None else str(exc) if "ix_topologies_name" in msg or "topologies.name" in msg: raise HTTPException( status_code=409, detail=f"A topology named {body.name!r} already exists.", ) from exc raise row = await repo.get_topology(topology_id) if row is None: # pragma: no cover — create then vanish raise HTTPException(status_code=500, detail="topology insert vanished") return row