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:
@@ -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()
|
||||
|
||||
@@ -113,6 +113,14 @@ class SubnetCreate(BaseModel):
|
||||
dhcp_enabled: bool = False
|
||||
|
||||
|
||||
class SubnetUpdate(BaseModel):
|
||||
network_id: str | None = None
|
||||
cidr: str | None = None
|
||||
gateway: str | None = None
|
||||
dns: list[str] | None = None
|
||||
dhcp_enabled: bool | None = None
|
||||
|
||||
|
||||
class SecurityGroupCreate(BaseModel):
|
||||
project_id: str | None = None
|
||||
name: str
|
||||
|
||||
Reference in New Issue
Block a user