feat(mazenet): auto-bridge new LANs to the DMZ gateway
When a non-DMZ LAN is created via POST /lans, look up the topology's gateway (decky with forwards_l3=True attached to the DMZ) and insert an edge binding it to the new LAN. The gateway becomes multi-homed to every internal LAN automatically, so DMZ_ORPHAN cannot arise from ordinary editor use. Also fixes delete_lan: the home-decky guard used scalar_one_or_none, which blew up when the gateway already had >1 'other' LAN edge. Switch to scalars().first() — we only need to know *some* other edge exists, not a unique one.
This commit is contained in:
@@ -1172,7 +1172,7 @@ class SQLModelRepository(BaseRepository):
|
||||
TopologyEdge.lan_id != lan_id,
|
||||
)
|
||||
)
|
||||
if other.scalar_one_or_none() is None:
|
||||
if other.scalars().first() is None:
|
||||
raise ValueError(
|
||||
f"cannot delete LAN {lan.name!r}: decky "
|
||||
f"{decky_uuid} has no other LAN (would be orphaned)"
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
|
||||
from decnet.logging import get_logger
|
||||
from decnet.telemetry import traced as _traced
|
||||
from decnet.topology.allocator import reserved_subnets
|
||||
from decnet.topology.status import (
|
||||
@@ -19,6 +20,7 @@ from decnet.web.dependencies import repo, require_admin
|
||||
|
||||
from ._guards import assert_pending_or_409, map_repo_exception
|
||||
|
||||
log = get_logger("api.topology.lan")
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -51,7 +53,7 @@ async def api_create_lan(
|
||||
from decnet.topology.allocator import SubnetAllocator
|
||||
|
||||
allocator = SubnetAllocator(
|
||||
"10.0.0.0/16", reserved=await reserved_subnets(repo)
|
||||
"10.0", reserved=await reserved_subnets(repo)
|
||||
)
|
||||
subnet = allocator.next_free()
|
||||
|
||||
@@ -74,9 +76,60 @@ async def api_create_lan(
|
||||
row = next((r for r in rows if r["id"] == lan_id), None)
|
||||
if row is None: # pragma: no cover — would mean insert vanished
|
||||
raise HTTPException(status_code=500, detail="LAN insert vanished")
|
||||
|
||||
# Auto-bridge: if this is a non-DMZ LAN, attach the topology's
|
||||
# DMZ gateway (the decky with forwards_l3=True that lives on the
|
||||
# DMZ LAN) to it. Satisfies the DMZ_ORPHAN invariant by
|
||||
# construction — every internal LAN always has a bridge path.
|
||||
if not body.is_dmz:
|
||||
try:
|
||||
await _auto_attach_gateway(topology_id, lan_id)
|
||||
except Exception as exc:
|
||||
# Best-effort: if the gateway is missing or the edge can't
|
||||
# be written, the deploy-time validator will surface it.
|
||||
log.warning(
|
||||
"auto-bridge skipped for LAN %s in topology %s: %s",
|
||||
lan_id, topology_id, exc,
|
||||
)
|
||||
|
||||
return LANRow(**row)
|
||||
|
||||
|
||||
async def _auto_attach_gateway(topology_id: str, new_lan_id: str) -> None:
|
||||
"""Attach the topology's DMZ gateway to a newly created non-DMZ LAN."""
|
||||
lans = await repo.list_lans_for_topology(topology_id)
|
||||
dmz = next((lan for lan in lans if lan.get("is_dmz")), None)
|
||||
if dmz is None:
|
||||
return
|
||||
|
||||
edges = await repo.list_topology_edges(topology_id)
|
||||
gateway_uuid: str | None = None
|
||||
for e in edges:
|
||||
if e["lan_id"] != dmz["id"]:
|
||||
continue
|
||||
if e.get("forwards_l3"):
|
||||
gateway_uuid = e["decky_uuid"]
|
||||
break
|
||||
if gateway_uuid is None:
|
||||
return
|
||||
|
||||
if any(
|
||||
e["decky_uuid"] == gateway_uuid and e["lan_id"] == new_lan_id
|
||||
for e in edges
|
||||
):
|
||||
return
|
||||
|
||||
await repo.add_topology_edge(
|
||||
{
|
||||
"topology_id": topology_id,
|
||||
"decky_uuid": gateway_uuid,
|
||||
"lan_id": new_lan_id,
|
||||
"is_bridge": True,
|
||||
"forwards_l3": True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{topology_id}/lans/{lan_id}",
|
||||
tags=["MazeNET Topologies"],
|
||||
|
||||
Reference in New Issue
Block a user