feat: add subnet edit functionality with internal subnet labeling in workload traffic insights

Add SubnetUpdate schema with optional fields for PATCH operations, implement update_subnet endpoint with validation and audit logging, add subnet_label_for_ip helper to match IPs against known subnets using longest prefix matching, update flow_endpoint_label to show "internal (CIDR)" for traffic within known subnets instead of "external", add DNS servers and DHCP toggle to subnet form UI, implement edit
This commit is contained in:
2026-07-09 15:29:36 +02:00
parent 10e9406510
commit a16b56614c
4 changed files with 135 additions and 15 deletions
+64 -2
View File
@@ -67,6 +67,7 @@ from app.schemas.domain import (
SetupStatus,
SubnetCreate,
SubnetRead,
SubnetUpdate,
TenantCreate,
TenantRead,
UserCreate,
@@ -246,6 +247,31 @@ def flow_int(value: object, default: int = 0) -> int:
return default
def subnet_label_for_ip(subnets: list[Subnet], value: str) -> str | None:
try:
address = ip_address(value)
except ValueError:
return None
matches: list[tuple[int, Subnet]] = []
for subnet in subnets:
try:
network = ip_network(subnet.cidr, strict=False)
except ValueError:
continue
if address in network:
matches.append((network.prefixlen, subnet))
if not matches:
return None
_, subnet = sorted(matches, key=lambda item: item[0], reverse=True)[0]
return f"internal ({subnet.cidr})"
def flow_endpoint_label(owner: Workload | None, subnets: list[Subnet], value: str) -> str:
if owner:
return owner.name
return subnet_label_for_ip(subnets, value) or "external"
def proxmox_action(action: str) -> str:
return {"allow": "ACCEPT", "deny": "DROP", "reject": "REJECT"}.get(action, "ACCEPT")
@@ -868,6 +894,7 @@ def workload_insights(workload_id: str, _: CurrentUser, db: Session = Depends(ge
address.address: db.get(Workload, address.workload_id)
for address in db.scalars(select(IpAddress).where(IpAddress.workload_id.is_not(None))).all()
}
known_subnets = db.scalars(select(Subnet).order_by(Subnet.cidr)).all()
traffic = []
if workload_ips:
flows = db.scalars(
@@ -881,8 +908,8 @@ def workload_insights(workload_id: str, _: CurrentUser, db: Session = Depends(ge
destination_owner = ip_owners.get(flow.destination_ip)
traffic.append(
{
"source": source_owner.name if source_owner else "external",
"destination": destination_owner.name if destination_owner else "external",
"source": flow_endpoint_label(source_owner, known_subnets, flow.source_ip),
"destination": flow_endpoint_label(destination_owner, known_subnets, flow.destination_ip),
"source_ip": flow.source_ip,
"destination_ip": flow.destination_ip,
"protocol": flow.protocol,
@@ -972,6 +999,41 @@ def create_subnet(payload: SubnetCreate, user: CurrentUser, db: Session = Depend
return subnet
@api_router.patch("/ipam/subnets/{subnet_id}", response_model=SubnetRead)
def update_subnet(subnet_id: str, payload: SubnetUpdate, user: CurrentUser, db: Session = Depends(get_db)) -> Subnet:
subnet = db.get(Subnet, subnet_id)
if not subnet:
raise HTTPException(status_code=404, detail="Subnet not found")
changes = payload.model_dump(exclude_unset=True)
if "cidr" in changes and not changes["cidr"]:
raise HTTPException(status_code=400, detail="CIDR is required")
if "network_id" in changes and not changes["network_id"]:
raise HTTPException(status_code=400, detail="Network is required")
if "network_id" in changes and changes["network_id"] and not db.get(Network, changes["network_id"]):
raise HTTPException(status_code=404, detail="Network not found")
old_values = {
"network_id": subnet.network_id,
"cidr": subnet.cidr,
"gateway": subnet.gateway,
"dns": subnet.dns,
"dhcp_enabled": subnet.dhcp_enabled,
}
for key, value in changes.items():
setattr(subnet, key, value)
commit_or_400(db)
db.refresh(subnet)
write_audit(
db,
action="ipam.subnet.updated",
object_type="subnet",
object_id=subnet.id,
user_id=user.id,
old_values=old_values,
new_values=changes,
)
return subnet
@api_router.get("/ipam/addresses", response_model=list[IpAddressRead])
def ipam_addresses(_: CurrentUser, db: Session = Depends(get_db)) -> list[dict]:
addresses = db.scalars(select(IpAddress).order_by(IpAddress.address)).all()