feat: add container network filtering, enhance IP address display, and improve workload insights UI
Add is_docker_or_container_network helper to detect Docker bridge, Kubernetes CNI, and loopback networks, implement cleanup_discovered_container_networks to remove container bridge IPs from discovered networks during IPAM discovery, add ip_address_payload helper to enrich IP addresses with subnet CIDR and workload details, update ProxmoxProvider to ignore guest interfaces matching common container pref
This commit is contained in:
@@ -109,6 +109,51 @@ def ensure_discovered_network(db: Session, cluster_id: str) -> Network:
|
||||
return network
|
||||
|
||||
|
||||
def is_docker_or_container_network(value: str) -> bool:
|
||||
try:
|
||||
interface = ip_interface(value)
|
||||
except ValueError:
|
||||
return False
|
||||
ip = interface.ip
|
||||
network = str(interface.network)
|
||||
if ip.is_loopback or ip.is_link_local:
|
||||
return True
|
||||
if ip.version == 4 and ip.packed[0] == 172 and 17 <= ip.packed[1] <= 31:
|
||||
return True
|
||||
return network.startswith(("10.42.", "10.43.", "10.244.", "10.245."))
|
||||
|
||||
|
||||
def ip_address_payload(db: Session, address: IpAddress) -> dict:
|
||||
subnet = db.get(Subnet, address.subnet_id)
|
||||
workload = db.get(Workload, address.workload_id) if address.workload_id else None
|
||||
return {
|
||||
"id": address.id,
|
||||
"subnet_id": address.subnet_id,
|
||||
"subnet_cidr": subnet.cidr if subnet else None,
|
||||
"address": address.address,
|
||||
"status": address.status,
|
||||
"workload_id": address.workload_id,
|
||||
"workload_name": workload.name if workload else None,
|
||||
"workload_external_id": workload.external_id if workload else None,
|
||||
"note": address.note,
|
||||
}
|
||||
|
||||
|
||||
def cleanup_discovered_container_networks(db: Session) -> int:
|
||||
removed = 0
|
||||
discovered_networks = db.scalars(select(Network).where(Network.name == "discovered-ipam")).all()
|
||||
for network in discovered_networks:
|
||||
subnets = db.scalars(select(Subnet).where(Subnet.network_id == network.id)).all()
|
||||
for subnet in subnets:
|
||||
if is_docker_or_container_network(subnet.cidr):
|
||||
addresses = db.scalars(select(IpAddress).where(IpAddress.subnet_id == subnet.id)).all()
|
||||
for address in addresses:
|
||||
db.delete(address)
|
||||
removed += 1
|
||||
db.delete(subnet)
|
||||
return removed
|
||||
|
||||
|
||||
def import_discovered_ips(db: Session, cluster_id: str, workload: Workload, addresses: list[str]) -> int:
|
||||
imported = 0
|
||||
for value in addresses:
|
||||
@@ -116,7 +161,7 @@ def import_discovered_ips(db: Session, cluster_id: str, workload: Workload, addr
|
||||
interface = ip_interface(value)
|
||||
except ValueError:
|
||||
continue
|
||||
if interface.ip.is_loopback or interface.ip.is_link_local:
|
||||
if is_docker_or_container_network(value):
|
||||
continue
|
||||
network = ensure_discovered_network(db, cluster_id)
|
||||
cidr = str(interface.network)
|
||||
@@ -408,7 +453,7 @@ def workload_insights(workload_id: str, _: CurrentUser, db: Session = Depends(ge
|
||||
policies = db.scalars(
|
||||
select(Policy).where((Policy.project_id == workload.project_id) | (Policy.project_id.is_(None))).order_by(Policy.name)
|
||||
).all()
|
||||
assigned_ips = db.scalars(select(IpAddress).where(IpAddress.workload_id == workload.id)).all()
|
||||
assigned_ips = db.scalars(select(IpAddress).where(IpAddress.workload_id == workload.id).order_by(IpAddress.address)).all()
|
||||
traffic = []
|
||||
audit_mode_notes = [
|
||||
f"{policy.name} is in audit mode; matching traffic is logged without enforcement."
|
||||
@@ -418,19 +463,8 @@ def workload_insights(workload_id: str, _: CurrentUser, db: Session = Depends(ge
|
||||
decision = "audit" if audit_mode_notes else "unknown"
|
||||
return WorkloadInsight(
|
||||
workload=workload,
|
||||
traffic=[
|
||||
{
|
||||
"source": workload.name,
|
||||
"destination": "unknown",
|
||||
"protocol": "unknown",
|
||||
"port": "unknown",
|
||||
"bytes": 0,
|
||||
"decision": "no_flow_telemetry",
|
||||
"ip_addresses": [address.address for address in assigned_ips],
|
||||
}
|
||||
]
|
||||
if assigned_ips
|
||||
else traffic,
|
||||
assigned_ips=[ip_address_payload(db, address) for address in assigned_ips],
|
||||
traffic=traffic,
|
||||
matching_policies=policies,
|
||||
effective_decision=decision,
|
||||
audit_mode_notes=audit_mode_notes,
|
||||
@@ -472,13 +506,15 @@ def create_subnet(payload: SubnetCreate, user: CurrentUser, db: Session = Depend
|
||||
|
||||
|
||||
@api_router.get("/ipam/addresses", response_model=list[IpAddressRead])
|
||||
def ipam_addresses(_: CurrentUser, db: Session = Depends(get_db)) -> list[IpAddress]:
|
||||
return db.scalars(select(IpAddress).order_by(IpAddress.address)).all()
|
||||
def ipam_addresses(_: CurrentUser, db: Session = Depends(get_db)) -> list[dict]:
|
||||
addresses = db.scalars(select(IpAddress).order_by(IpAddress.address)).all()
|
||||
return [ip_address_payload(db, address) for address in addresses]
|
||||
|
||||
|
||||
@api_router.post("/ipam/discover")
|
||||
async def discover_ipam(user: CurrentUser, db: Session = Depends(get_db)) -> dict:
|
||||
imported = 0
|
||||
removed = cleanup_discovered_container_networks(db)
|
||||
errors = []
|
||||
clusters = db.scalars(select(Cluster).order_by(Cluster.name)).all()
|
||||
for cluster in clusters:
|
||||
@@ -502,10 +538,18 @@ async def discover_ipam(user: CurrentUser, db: Session = Depends(get_db)) -> dic
|
||||
imported += import_discovered_ips(db, cluster.id, workload, raw_workload.get("ip_addresses", []))
|
||||
except Exception as exc:
|
||||
errors.append({"cluster": cluster.name, "error": str(exc)})
|
||||
db.add(Job(kind="ipam.discover", status="success" if not errors else "failed", progress=100, logs=[f"Imported {imported} IP addresses"], error=str(errors) if errors else None))
|
||||
db.add(
|
||||
Job(
|
||||
kind="ipam.discover",
|
||||
status="success" if not errors else "failed",
|
||||
progress=100,
|
||||
logs=[f"Imported {imported} IP addresses", f"Removed {removed} container bridge IPs"],
|
||||
error=str(errors) if errors else None,
|
||||
)
|
||||
)
|
||||
commit_or_400(db)
|
||||
write_audit(db, action="ipam.discover", object_type="ipam", user_id=user.id, new_values={"imported": imported, "errors": errors}, result="success" if not errors else "failed")
|
||||
return {"imported": imported, "errors": errors}
|
||||
write_audit(db, action="ipam.discover", object_type="ipam", user_id=user.id, new_values={"imported": imported, "removed": removed, "errors": errors}, result="success" if not errors else "failed")
|
||||
return {"imported": imported, "removed": removed, "errors": errors}
|
||||
|
||||
|
||||
@api_router.post("/ipam/addresses", response_model=IpAddressRead)
|
||||
@@ -517,7 +561,7 @@ def reserve_ip(payload: IpReservationCreate, user: CurrentUser, db: Session = De
|
||||
commit_or_400(db)
|
||||
db.refresh(address)
|
||||
write_audit(db, action="ipam.address.created", object_type="ip_address", object_id=address.id, user_id=user.id)
|
||||
return address
|
||||
return ip_address_payload(db, address)
|
||||
|
||||
|
||||
@api_router.patch("/ipam/addresses/{address_id}", response_model=IpAddressRead)
|
||||
@@ -541,7 +585,7 @@ def update_ip(address_id: str, payload: IpReservationCreate, user: CurrentUser,
|
||||
old_values=old_values,
|
||||
new_values={"address": address.address, "status": address.status, "note": address.note},
|
||||
)
|
||||
return address
|
||||
return ip_address_payload(db, address)
|
||||
|
||||
|
||||
@api_router.delete("/ipam/addresses/{address_id}")
|
||||
|
||||
Reference in New Issue
Block a user