test nic state link api
All checks were successful
Build and Deploy MITM Webserver / traffic_target (push) Successful in 1s
Build and Deploy MITM Webserver / build (push) Successful in 10s

This commit is contained in:
2026-03-09 15:29:53 +01:00
parent d2179b2813
commit 4c491cf4b1
7 changed files with 799 additions and 22 deletions

View File

@@ -1,11 +1,15 @@
"""Network inspection and bridge management endpoints."""
from typing import List, Optional
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from pyroute2 import IPRoute, NDB
from src.config import settings
from src.utilities.bridge_link_state_manager import bridge_link_state_manager
from src.utilities.interface_bridge_helpers import get_bridge_ports_once
router = APIRouter()
ip: IPRoute | None = None
@@ -79,6 +83,41 @@ class BridgeRemoveRequest(BaseModel):
name: str
class BridgeLinkStateEnableRequest(BaseModel):
"""Payload for enabling bridge member link-state propagation."""
poll_interval_seconds: float = Field(
settings.bridge_link_state_poll_interval_seconds,
gt=0,
description="Polling interval for member link-state checks.",
)
class BridgeMemberLinkStateInfo(BaseModel):
"""Current link-state snapshot for one bridge member."""
ifname: str = Field(..., description="Interface name.")
admin_up: Optional[bool] = Field(None, description="Whether the interface has IFF_UP set.")
carrier_up: Optional[bool] = Field(None, description="Whether the interface currently reports carrier.")
operstate: Optional[str] = Field(None, description="Kernel operational state string.")
link_ready: bool = Field(..., description="Whether the member currently looks usable for forwarding.")
suppressed: bool = Field(..., description="Whether the watcher administratively suppressed this member.")
class BridgeLinkStateWatcherStatus(BaseModel):
"""Status for one bridge link-state propagation watcher."""
bridge: str = Field(..., description="Bridge interface name.")
active: bool = Field(..., description="Whether the watcher thread is currently active.")
poll_interval_seconds: Optional[float] = Field(None, description="Watcher polling interval.")
last_poll_ts: Optional[float] = Field(None, description="Unix timestamp of the last poll.")
last_error: Optional[str] = Field(None, description="Most recent watcher error, if any.")
last_action: Optional[str] = Field(None, description="Most recent propagation action.")
suppressed_members: List[str] = Field(default_factory=list, description="Members currently forced down by the watcher.")
members: Dict[str, BridgeMemberLinkStateInfo] = Field(default_factory=dict, description="Per-member link-state snapshot.")
message: Optional[str] = Field(None, description="Optional informational message.")
def init_network_api() -> None:
"""Initialize lazy pyroute2 clients."""
global ip, ndb
@@ -91,6 +130,7 @@ def init_network_api() -> None:
def shutdown_network_api() -> None:
"""Close pyroute2 clients if they were initialized."""
global ip, ndb
bridge_link_state_manager.stop()
if ip:
ip.close()
ip = None
@@ -167,6 +207,11 @@ def bridge_exists(name: str, ip_route: IPRoute) -> bool:
return bool(ip_route.link_lookup(ifname=name))
def _watcher_status_response(payload: dict[str, Any]) -> BridgeLinkStateWatcherStatus:
"""Convert an internal watcher status dictionary to the API response model."""
return BridgeLinkStateWatcherStatus.model_validate(payload)
@router.get("/interfaces", response_model=List[InterfaceInfo])
def get_interfaces(ip: IPRoute = Depends(get_iproute)) -> List[InterfaceInfo]:
"""List host interfaces with addresses and decoded flags."""
@@ -346,3 +391,64 @@ def remove_bridge(req: BridgeRemoveRequest, ip: IPRoute = Depends(get_iproute))
"status": "ok",
"deleted": req.name,
}
@router.get("/bridge/link-state-watchers", response_model=List[BridgeLinkStateWatcherStatus])
def list_bridge_link_state_watchers() -> List[BridgeLinkStateWatcherStatus]:
"""List all bridge member link-state propagation watchers."""
return [_watcher_status_response(status) for status in bridge_link_state_manager.list_statuses()]
@router.get("/bridge/{bridge_name}/link-state-watcher", response_model=BridgeLinkStateWatcherStatus)
def get_bridge_link_state_watcher(
bridge_name: str,
ip: IPRoute = Depends(get_iproute),
) -> BridgeLinkStateWatcherStatus:
"""Return the watcher status for one bridge."""
if not bridge_exists(bridge_name, ip):
raise HTTPException(status_code=404, detail=f"Bridge {bridge_name} not found")
status = bridge_link_state_manager.get_status(bridge_name)
if status is None:
return BridgeLinkStateWatcherStatus(
bridge=bridge_name,
active=False,
message="watcher not enabled",
)
return _watcher_status_response(status)
@router.post("/bridge/{bridge_name}/link-state-watcher/enable", response_model=BridgeLinkStateWatcherStatus)
def enable_bridge_link_state_watcher(
bridge_name: str,
req: BridgeLinkStateEnableRequest,
ip: IPRoute = Depends(get_iproute),
) -> BridgeLinkStateWatcherStatus:
"""Enable member link-state propagation for a bridge."""
if not bridge_exists(bridge_name, ip):
raise HTTPException(status_code=404, detail=f"Bridge {bridge_name} not found")
members = get_bridge_ports_once(bridge_name)
if len(members) < 2:
raise HTTPException(
status_code=400,
detail=f"Bridge {bridge_name} must have at least two member interfaces",
)
status = bridge_link_state_manager.enable(
bridge_name=bridge_name,
poll_interval_seconds=req.poll_interval_seconds,
)
return _watcher_status_response(status)
@router.post("/bridge/{bridge_name}/link-state-watcher/disable", response_model=BridgeLinkStateWatcherStatus)
def disable_bridge_link_state_watcher(
bridge_name: str,
ip: IPRoute = Depends(get_iproute),
) -> BridgeLinkStateWatcherStatus:
"""Disable member link-state propagation for a bridge."""
if not bridge_exists(bridge_name, ip):
raise HTTPException(status_code=404, detail=f"Bridge {bridge_name} not found")
return _watcher_status_response(bridge_link_state_manager.disable(bridge_name))