Skip to content

Commit f5fbece

Browse files
Feat add protocol traffic engineering (#263)
* Adding protocol traffic-engineering backend * Adding protocol traffic-engineering frontend
1 parent db9e8de commit f5fbece

23 files changed

Lines changed: 1750 additions & 12 deletions

File tree

backend/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
from routers.rip import rip as rip_router
6868
from routers.ripng import ripng as ripng_router
6969
from routers.rpki import rpki as rpki_router
70+
from routers.traffic_engineering import traffic_engineering as te_router
7071
from routers import version as version_router
7172
from routers import events as events_router
7273
from routers.events import start_poller, stop_poller
@@ -354,6 +355,7 @@ async def get_permissions(request: Request) -> dict:
354355
app.include_router(rip_router.router)
355356
app.include_router(ripng_router.router)
356357
app.include_router(rpki_router.router)
358+
app.include_router(te_router.router)
357359
app.include_router(version_router.router)
358360
app.include_router(events_router.router)
359361

backend/rbac_permissions.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class FeatureGroup(str, Enum):
7979
SEGMENT_ROUTING = "SEGMENT_ROUTING"
8080
NHRP = "NHRP"
8181
RPKI = "RPKI"
82+
TRAFFIC_ENGINEERING = "TRAFFIC_ENGINEERING"
8283

8384
# Routing Policies
8485
ROUTING_POLICIES = "ROUTING_POLICIES"
@@ -173,6 +174,7 @@ class BuiltInRole(str, Enum):
173174
FeatureGroup.SEGMENT_ROUTING: PermissionLevel.WRITE,
174175
FeatureGroup.NHRP: PermissionLevel.WRITE,
175176
FeatureGroup.RPKI: PermissionLevel.WRITE,
177+
FeatureGroup.TRAFFIC_ENGINEERING: PermissionLevel.WRITE,
176178
FeatureGroup.ROUTING_POLICIES: PermissionLevel.WRITE,
177179
FeatureGroup.ACCESS_LIST: PermissionLevel.WRITE,
178180
FeatureGroup.PREFIX_LIST: PermissionLevel.WRITE,
@@ -241,6 +243,7 @@ class BuiltInRole(str, Enum):
241243
FeatureGroup.SEGMENT_ROUTING: PermissionLevel.WRITE,
242244
FeatureGroup.NHRP: PermissionLevel.WRITE,
243245
FeatureGroup.RPKI: PermissionLevel.WRITE,
246+
FeatureGroup.TRAFFIC_ENGINEERING: PermissionLevel.WRITE,
244247
FeatureGroup.ROUTING_POLICIES: PermissionLevel.WRITE,
245248
FeatureGroup.ACCESS_LIST: PermissionLevel.WRITE,
246249
FeatureGroup.PREFIX_LIST: PermissionLevel.WRITE,
@@ -310,6 +313,7 @@ class BuiltInRole(str, Enum):
310313
FeatureGroup.SEGMENT_ROUTING: PermissionLevel.READ,
311314
FeatureGroup.NHRP: PermissionLevel.READ,
312315
FeatureGroup.RPKI: PermissionLevel.READ,
316+
FeatureGroup.TRAFFIC_ENGINEERING: PermissionLevel.READ,
313317
FeatureGroup.ROUTING_POLICIES: PermissionLevel.READ,
314318
FeatureGroup.ACCESS_LIST: PermissionLevel.READ,
315319
FeatureGroup.PREFIX_LIST: PermissionLevel.READ,
@@ -419,6 +423,7 @@ async def get_user_permissions(
419423
FeatureGroup.SEGMENT_ROUTING,
420424
FeatureGroup.NHRP,
421425
FeatureGroup.RPKI,
426+
FeatureGroup.TRAFFIC_ENGINEERING,
422427
FeatureGroup.ROUTING_POLICIES,
423428
FeatureGroup.ACCESS_LIST,
424429
FeatureGroup.PREFIX_LIST,
@@ -506,6 +511,7 @@ async def get_user_permissions(
506511
FeatureGroup.SEGMENT_ROUTING,
507512
FeatureGroup.NHRP,
508513
FeatureGroup.RPKI,
514+
FeatureGroup.TRAFFIC_ENGINEERING,
509515
FeatureGroup.ROUTING_POLICIES,
510516
FeatureGroup.ACCESS_LIST,
511517
FeatureGroup.PREFIX_LIST,
@@ -703,7 +709,7 @@ def _apply_parent_child_permissions(permissions: Dict[FeatureGroup, PermissionLe
703709
- VPN → IPSEC, WIREGUARD
704710
- ROUTING → UNICAST_PROTOCOLS
705711
- UNICAST_PROTOCOLS → BGP, OSPF, OSPFv3, ISIS, OPENFABRIC, RIP, RIPng, BABEL
706-
- ROUTING_INFRASTRUCTURE → BFD, MPLS, SEGMENT_ROUTING, NHRP, RPKI
712+
- ROUTING_INFRASTRUCTURE → BFD, MPLS, SEGMENT_ROUTING, NHRP, RPKI, TRAFFIC_ENGINEERING
707713
- ROUTING_POLICIES → ACCESS_LIST, PREFIX_LIST, ROUTE_POLICY, ROUTE_MAP, LOCAL_ROUTE,
708714
BGP_AS_PATH, BGP_COMMUNITY, BGP_EXTENDED_COMMUNITY, BGP_LARGE_COMMUNITY
709715
- MULTICAST → IGMP_PROXY, PIM, PIM6
@@ -779,6 +785,7 @@ def _apply_parent_child_permissions(permissions: Dict[FeatureGroup, PermissionLe
779785
FeatureGroup.SEGMENT_ROUTING,
780786
FeatureGroup.NHRP,
781787
FeatureGroup.RPKI,
788+
FeatureGroup.TRAFFIC_ENGINEERING,
782789
]
783790
for child in infrastructure_children:
784791
current = permissions.get(child, PermissionLevel.NONE)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .traffic_engineering import router
2+
3+
__all__ = ["router"]
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"""Traffic Engineering Protocol Router.
2+
3+
API endpoints for managing VyOS Traffic Engineering configuration.
4+
Traffic Engineering is only supported on VyOS 1.5+; on 1.4 the capabilities
5+
endpoint advertises the feature as unsupported and the config/batch endpoints
6+
return early with empty data.
7+
"""
8+
9+
from fastapi import APIRouter, HTTPException, Request
10+
from starlette.concurrency import run_in_threadpool
11+
from pydantic import BaseModel, Field
12+
from typing import List, Optional, Dict, Any
13+
from session_vyos_service import get_session_vyos_service
14+
from vyos_builders import TrafficEngineeringBatchBuilder
15+
from fastapi_permissions import require_read_permission, require_write_permission
16+
from rbac_permissions import FeatureGroup
17+
import inspect
18+
import logging
19+
20+
logger = logging.getLogger(__name__)
21+
22+
router = APIRouter(prefix="/vyos/traffic-engineering", tags=["traffic-engineering"])
23+
24+
25+
# ============================================================================
26+
# Pydantic Models
27+
# ============================================================================
28+
29+
30+
class AdminGroup(BaseModel):
31+
name: str
32+
bit_position: Optional[int] = None
33+
34+
35+
class TeInterface(BaseModel):
36+
name: str
37+
admin_groups: List[str] = []
38+
max_bandwidth: Optional[int] = None
39+
max_reservable_bandwidth: Optional[int] = None
40+
metric: Optional[int] = None
41+
42+
43+
class TrafficEngineeringConfig(BaseModel):
44+
admin_groups: List[AdminGroup] = []
45+
interfaces: List[TeInterface] = []
46+
47+
48+
class TrafficEngineeringBatchOperation(BaseModel):
49+
op: str = Field(..., description="Builder method name")
50+
value: Optional[str] = Field(None, description="Comma-separated arguments")
51+
52+
53+
class TrafficEngineeringBatchRequest(BaseModel):
54+
operations: List[TrafficEngineeringBatchOperation]
55+
56+
57+
class VyOSResponse(BaseModel):
58+
success: bool
59+
data: Optional[Dict[str, Any]] = None
60+
error: Optional[str] = None
61+
62+
63+
# ============================================================================
64+
# Endpoint 1: Capabilities
65+
# ============================================================================
66+
67+
68+
@router.get("/capabilities")
69+
async def get_te_capabilities(request: Request):
70+
"""Return Traffic Engineering feature capabilities based on the device VyOS version."""
71+
await require_read_permission(request, FeatureGroup.TRAFFIC_ENGINEERING)
72+
73+
try:
74+
service = get_session_vyos_service(request)
75+
builder = TrafficEngineeringBatchBuilder(version=service.get_version())
76+
capabilities = builder.get_capabilities()
77+
78+
if hasattr(request.state, "instance") and request.state.instance:
79+
capabilities["instance_name"] = request.state.instance.get("name")
80+
capabilities["instance_id"] = request.state.instance.get("id")
81+
82+
return capabilities
83+
except Exception:
84+
logger.exception("Unhandled error in get_te_capabilities")
85+
raise HTTPException(status_code=500, detail="Internal server error")
86+
87+
88+
# ============================================================================
89+
# Endpoint 2: Config
90+
# ============================================================================
91+
92+
93+
@router.get("/config", response_model=TrafficEngineeringConfig)
94+
async def get_te_config(http_request: Request, refresh: bool = False):
95+
"""Return the full Traffic Engineering configuration in a normalized format."""
96+
await require_read_permission(http_request, FeatureGroup.TRAFFIC_ENGINEERING)
97+
98+
try:
99+
service = get_session_vyos_service(http_request)
100+
version = service.get_version()
101+
102+
if "1.4" in version:
103+
return TrafficEngineeringConfig()
104+
105+
full_config = await run_in_threadpool(service.get_full_config, refresh=refresh)
106+
te_raw = full_config.get("protocols", {}).get("traffic-engineering", {})
107+
108+
if not te_raw:
109+
return TrafficEngineeringConfig()
110+
111+
return TrafficEngineeringConfig(
112+
admin_groups=_parse_admin_groups(te_raw.get("admin-group", {})),
113+
interfaces=_parse_interfaces(te_raw.get("interface", {})),
114+
)
115+
except Exception:
116+
logger.exception("Unhandled error in get_te_config")
117+
raise HTTPException(status_code=500, detail="Internal server error")
118+
119+
120+
# ============================================================================
121+
# Config Parsers
122+
# ============================================================================
123+
124+
125+
def _safe_int(value) -> Optional[int]:
126+
if value is None:
127+
return None
128+
try:
129+
return int(value)
130+
except (ValueError, TypeError):
131+
return None
132+
133+
134+
def _parse_admin_groups(raw: dict) -> List[AdminGroup]:
135+
if not raw:
136+
return []
137+
138+
groups = []
139+
for name, cfg in raw.items():
140+
if cfg is None:
141+
cfg = {}
142+
groups.append(AdminGroup(
143+
name=name,
144+
bit_position=_safe_int(cfg.get("bit-position")),
145+
))
146+
return groups
147+
148+
149+
def _parse_interfaces(raw: dict) -> List[TeInterface]:
150+
if not raw:
151+
return []
152+
153+
interfaces = []
154+
for iface_name, cfg in raw.items():
155+
if cfg is None:
156+
cfg = {}
157+
158+
# admin-group is multi-value: may be a list or a single string
159+
raw_groups = cfg.get("admin-group", [])
160+
if isinstance(raw_groups, str):
161+
admin_groups = [raw_groups]
162+
elif isinstance(raw_groups, list):
163+
admin_groups = raw_groups
164+
else:
165+
admin_groups = list(raw_groups) if raw_groups else []
166+
167+
interfaces.append(TeInterface(
168+
name=iface_name,
169+
admin_groups=admin_groups,
170+
max_bandwidth=_safe_int(cfg.get("max-bandwidth")),
171+
max_reservable_bandwidth=_safe_int(cfg.get("max-reservable-bandwidth")),
172+
metric=_safe_int(cfg.get("metric")),
173+
))
174+
return interfaces
175+
176+
177+
# ============================================================================
178+
# Endpoint 3: Batch Operations
179+
# ============================================================================
180+
181+
182+
@router.post("/batch", response_model=VyOSResponse)
183+
async def te_batch_configure(http_request: Request, body: TrafficEngineeringBatchRequest):
184+
"""Execute a batch of Traffic Engineering configuration operations atomically."""
185+
await require_write_permission(http_request, FeatureGroup.TRAFFIC_ENGINEERING)
186+
187+
try:
188+
service = get_session_vyos_service(http_request)
189+
190+
if "1.4" in service.get_version():
191+
raise HTTPException(
192+
status_code=400,
193+
detail="Traffic Engineering is not supported on VyOS 1.4",
194+
)
195+
196+
builder = TrafficEngineeringBatchBuilder(version=service.get_version())
197+
198+
for operation in body.operations:
199+
method = getattr(builder, operation.op)
200+
sig = inspect.signature(method)
201+
params = [p for p in sig.parameters.keys() if p != "self"]
202+
203+
if len(params) == 0:
204+
method()
205+
elif len(params) == 1:
206+
if operation.value is not None:
207+
method(operation.value)
208+
elif len(params) == 2 and operation.value is not None:
209+
values = operation.value.split(",", 1)
210+
if len(values) == 2:
211+
method(values[0], values[1])
212+
else:
213+
method(operation.value, "")
214+
215+
response = service.execute_batch(builder)
216+
217+
return VyOSResponse(
218+
success=response.status == 200,
219+
data={"message": "Traffic Engineering configuration updated"},
220+
error=response.error if response.error else None,
221+
)
222+
except HTTPException:
223+
raise
224+
except AttributeError as e:
225+
raise HTTPException(status_code=400, detail=f"Unknown operation: {str(e)}")
226+
except Exception:
227+
logger.exception("Unhandled error in te_batch_configure")
228+
raise HTTPException(status_code=500, detail="Internal server error")

backend/vyos_builders/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from .rip import RipBatchBuilder
4141
from .ripng import RipNgBatchBuilder
4242
from .rpki import RpkiBatchBuilder
43+
from .traffic_engineering import TrafficEngineeringBatchBuilder
4344

4445
# Directly use the self-contained builders
4546
EthernetBatchBuilder = EthernetInterfaceBuilderMixin
@@ -101,6 +102,7 @@
101102
"RipBatchBuilder",
102103
"RipNgBatchBuilder",
103104
"RpkiBatchBuilder",
105+
"TrafficEngineeringBatchBuilder",
104106
"BondingBatchBuilder",
105107
"GeneveBatchBuilder",
106108
"InputBatchBuilder",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Traffic Engineering Protocol Builder Package."""
2+
from .traffic_engineering_batch_builder import TrafficEngineeringBatchBuilder
3+
4+
__all__ = ["TrafficEngineeringBatchBuilder"]

0 commit comments

Comments
 (0)