Skip to content

Commit a8aa9b9

Browse files
Feat add protocol ri png support (#260)
* Add protocol RIPng backend * Add protocol RIPng frontend
1 parent 495f1a2 commit a8aa9b9

19 files changed

Lines changed: 3180 additions & 1 deletion

File tree

backend/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
from routers.pim import pim as pim_router
6666
from routers.pim6 import pim6 as pim6_router
6767
from routers.rip import rip as rip_router
68+
from routers.ripng import ripng as ripng_router
6869
from routers import version as version_router
6970
from routers import events as events_router
7071
from routers.events import start_poller, stop_poller
@@ -350,6 +351,7 @@ async def get_permissions(request: Request) -> dict:
350351
app.include_router(pim_router.router)
351352
app.include_router(pim6_router.router)
352353
app.include_router(rip_router.router)
354+
app.include_router(ripng_router.router)
353355
app.include_router(version_router.router)
354356
app.include_router(events_router.router)
355357

backend/routers/ripng/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""RIPng Protocol Router Package."""

backend/routers/ripng/ripng.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
"""RIPng Protocol Router.
2+
3+
API endpoints for managing VyOS RIPng (Routing Information Protocol next generation)
4+
configuration. Supports version-aware configuration for VyOS 1.4 and 1.5.
5+
"""
6+
7+
from fastapi import APIRouter, HTTPException, Request
8+
from starlette.concurrency import run_in_threadpool
9+
from pydantic import BaseModel, Field
10+
from typing import List, Dict, Optional, Any
11+
from session_vyos_service import get_session_vyos_service
12+
from vyos_builders import RipNgBatchBuilder
13+
from fastapi_permissions import require_read_permission, require_write_permission
14+
from rbac_permissions import FeatureGroup
15+
import inspect
16+
import logging
17+
18+
logger = logging.getLogger(__name__)
19+
20+
router = APIRouter(prefix="/vyos/ripng", tags=["ripng"])
21+
22+
23+
# ============================================================================
24+
# Pydantic Models
25+
# ============================================================================
26+
27+
28+
class RipNgDistributeListGlobal(BaseModel):
29+
access_list_in: Optional[str] = None
30+
access_list_out: Optional[str] = None
31+
prefix_list_in: Optional[str] = None
32+
prefix_list_out: Optional[str] = None
33+
34+
35+
class RipNgDistributeListInterface(BaseModel):
36+
interface: str
37+
access_list_in: Optional[str] = None
38+
access_list_out: Optional[str] = None
39+
prefix_list_in: Optional[str] = None
40+
prefix_list_out: Optional[str] = None
41+
42+
43+
class RipNgDistributeList(BaseModel):
44+
global_filters: RipNgDistributeListGlobal = RipNgDistributeListGlobal()
45+
interface_filters: List[RipNgDistributeListInterface] = []
46+
47+
48+
class RipNgInterface(BaseModel):
49+
name: str
50+
split_horizon: Optional[str] = None
51+
52+
53+
class RipNgRedistribute(BaseModel):
54+
protocol: str
55+
metric: Optional[int] = None
56+
route_map: Optional[str] = None
57+
58+
59+
class RipNgTimers(BaseModel):
60+
update: Optional[int] = None
61+
timeout: Optional[int] = None
62+
garbage_collection: Optional[int] = None
63+
64+
65+
class RipNgConfig(BaseModel):
66+
default_information_originate: bool = False
67+
default_metric: Optional[int] = None
68+
route_map: Optional[str] = None
69+
aggregate_addresses: List[str] = []
70+
networks: List[str] = []
71+
routes: List[str] = []
72+
passive_interfaces: List[str] = []
73+
distribute_list: RipNgDistributeList = RipNgDistributeList()
74+
interfaces: List[RipNgInterface] = []
75+
redistribute: List[RipNgRedistribute] = []
76+
timers: RipNgTimers = RipNgTimers()
77+
78+
79+
class RipNgBatchOperation(BaseModel):
80+
op: str = Field(..., description="Builder method name")
81+
value: Optional[str] = Field(None, description="Comma-separated arguments")
82+
83+
84+
class RipNgBatchRequest(BaseModel):
85+
operations: List[RipNgBatchOperation]
86+
87+
88+
class VyOSResponse(BaseModel):
89+
success: bool
90+
data: Optional[Dict[str, Any]] = None
91+
error: Optional[str] = None
92+
93+
94+
# ============================================================================
95+
# Endpoint 1: Capabilities
96+
# ============================================================================
97+
98+
99+
@router.get("/capabilities")
100+
async def get_ripng_capabilities(request: Request):
101+
"""Return RIPng feature capabilities based on the device VyOS version."""
102+
await require_read_permission(request, FeatureGroup.RIPNG)
103+
104+
try:
105+
service = get_session_vyos_service(request)
106+
builder = RipNgBatchBuilder(version=service.get_version())
107+
capabilities = builder.get_capabilities()
108+
109+
if hasattr(request.state, "instance") and request.state.instance:
110+
capabilities["instance_name"] = request.state.instance.get("name")
111+
capabilities["instance_id"] = request.state.instance.get("id")
112+
113+
return capabilities
114+
except Exception:
115+
logger.exception("Unhandled error in get_ripng_capabilities")
116+
raise HTTPException(status_code=500, detail="Internal server error")
117+
118+
119+
# ============================================================================
120+
# Endpoint 2: Config
121+
# ============================================================================
122+
123+
124+
@router.get("/config", response_model=RipNgConfig)
125+
async def get_ripng_config(http_request: Request, refresh: bool = False):
126+
"""Return the full RIPng configuration in a normalized format."""
127+
await require_read_permission(http_request, FeatureGroup.RIPNG)
128+
129+
try:
130+
service = get_session_vyos_service(http_request)
131+
full_config = await run_in_threadpool(service.get_full_config, refresh=refresh)
132+
133+
ripng_raw = full_config.get("protocols", {}).get("ripng", {})
134+
135+
if not ripng_raw:
136+
return RipNgConfig()
137+
138+
return RipNgConfig(
139+
default_information_originate="originate" in (ripng_raw.get("default-information") or {}),
140+
default_metric=_safe_int(ripng_raw.get("default-metric")),
141+
route_map=ripng_raw.get("route-map"),
142+
aggregate_addresses=_to_list(ripng_raw.get("aggregate-address")),
143+
networks=_to_list(ripng_raw.get("network")),
144+
routes=_to_list(ripng_raw.get("route")),
145+
passive_interfaces=_to_list(ripng_raw.get("passive-interface")),
146+
distribute_list=_parse_distribute_list(ripng_raw.get("distribute-list", {})),
147+
interfaces=_parse_interfaces(ripng_raw.get("interface", {})),
148+
redistribute=_parse_redistribute(ripng_raw.get("redistribute", {})),
149+
timers=_parse_timers(ripng_raw.get("timers", {})),
150+
)
151+
except Exception:
152+
logger.exception("Unhandled error in get_ripng_config")
153+
raise HTTPException(status_code=500, detail="Internal server error")
154+
155+
156+
# ============================================================================
157+
# Config Parsers
158+
# ============================================================================
159+
160+
161+
def _safe_int(value) -> Optional[int]:
162+
if value is None:
163+
return None
164+
try:
165+
return int(value)
166+
except (ValueError, TypeError):
167+
return None
168+
169+
170+
def _to_list(value) -> List[str]:
171+
if value is None:
172+
return []
173+
if isinstance(value, list):
174+
return value
175+
if isinstance(value, str):
176+
return [value]
177+
if isinstance(value, dict):
178+
return list(value.keys())
179+
return []
180+
181+
182+
def _parse_distribute_list(raw: dict) -> RipNgDistributeList:
183+
if not raw:
184+
return RipNgDistributeList()
185+
186+
acl_raw = raw.get("access-list", {}) or {}
187+
pl_raw = raw.get("prefix-list", {}) or {}
188+
189+
global_filters = RipNgDistributeListGlobal(
190+
access_list_in=acl_raw.get("in") if isinstance(acl_raw, dict) else None,
191+
access_list_out=acl_raw.get("out") if isinstance(acl_raw, dict) else None,
192+
prefix_list_in=pl_raw.get("in") if isinstance(pl_raw, dict) else None,
193+
prefix_list_out=pl_raw.get("out") if isinstance(pl_raw, dict) else None,
194+
)
195+
196+
iface_filters: List[RipNgDistributeListInterface] = []
197+
for iface, iface_raw in (raw.get("interface", {}) or {}).items():
198+
if iface_raw is None:
199+
iface_raw = {}
200+
iface_acl = iface_raw.get("access-list", {}) or {}
201+
iface_pl = iface_raw.get("prefix-list", {}) or {}
202+
iface_filters.append(RipNgDistributeListInterface(
203+
interface=iface,
204+
access_list_in=iface_acl.get("in") if isinstance(iface_acl, dict) else None,
205+
access_list_out=iface_acl.get("out") if isinstance(iface_acl, dict) else None,
206+
prefix_list_in=iface_pl.get("in") if isinstance(iface_pl, dict) else None,
207+
prefix_list_out=iface_pl.get("out") if isinstance(iface_pl, dict) else None,
208+
))
209+
210+
return RipNgDistributeList(global_filters=global_filters, interface_filters=iface_filters)
211+
212+
213+
def _parse_interfaces(raw: dict) -> List[RipNgInterface]:
214+
if not raw:
215+
return []
216+
217+
interfaces = []
218+
for iface_name, cfg in raw.items():
219+
if cfg is None:
220+
cfg = {}
221+
222+
split_horizon_raw = cfg.get("split-horizon", {}) or {}
223+
split_horizon = None
224+
if "poison-reverse" in split_horizon_raw:
225+
split_horizon = "poison-reverse"
226+
elif "disable" in split_horizon_raw:
227+
split_horizon = "disable"
228+
229+
interfaces.append(RipNgInterface(
230+
name=iface_name,
231+
split_horizon=split_horizon,
232+
))
233+
234+
return interfaces
235+
236+
237+
def _parse_redistribute(raw: dict) -> List[RipNgRedistribute]:
238+
if not raw:
239+
return []
240+
241+
entries = []
242+
for protocol, cfg in raw.items():
243+
if cfg is None:
244+
cfg = {}
245+
entries.append(RipNgRedistribute(
246+
protocol=protocol,
247+
metric=_safe_int(cfg.get("metric")),
248+
route_map=cfg.get("route-map"),
249+
))
250+
return entries
251+
252+
253+
def _parse_timers(raw: dict) -> RipNgTimers:
254+
if not raw:
255+
return RipNgTimers()
256+
return RipNgTimers(
257+
update=_safe_int(raw.get("update")),
258+
timeout=_safe_int(raw.get("timeout")),
259+
garbage_collection=_safe_int(raw.get("garbage-collection")),
260+
)
261+
262+
263+
# ============================================================================
264+
# Endpoint 3: Batch Operations
265+
# ============================================================================
266+
267+
268+
@router.post("/batch", response_model=VyOSResponse)
269+
async def ripng_batch_configure(http_request: Request, body: RipNgBatchRequest):
270+
"""Execute a batch of RIPng configuration operations atomically."""
271+
await require_write_permission(http_request, FeatureGroup.RIPNG)
272+
273+
try:
274+
service = get_session_vyos_service(http_request)
275+
builder = RipNgBatchBuilder(version=service.get_version())
276+
277+
for operation in body.operations:
278+
method = getattr(builder, operation.op)
279+
sig = inspect.signature(method)
280+
params = [p for p in sig.parameters.keys() if p != "self"]
281+
282+
if len(params) == 0:
283+
method()
284+
elif len(params) == 1:
285+
if operation.value is not None:
286+
method(operation.value)
287+
elif len(params) == 2 and operation.value is not None:
288+
values = operation.value.split(",", 1)
289+
if len(values) == 2:
290+
method(values[0], values[1])
291+
else:
292+
method(operation.value, "")
293+
elif len(params) == 3 and operation.value is not None:
294+
values = operation.value.split(",", 2)
295+
if len(values) == 3:
296+
method(values[0], values[1], values[2])
297+
elif len(values) == 2:
298+
method(values[0], values[1], "")
299+
300+
response = service.execute_batch(builder)
301+
302+
return VyOSResponse(
303+
success=response.status == 200,
304+
data={"message": "RIPng configuration updated"},
305+
error=response.error if response.error else None,
306+
)
307+
except AttributeError as e:
308+
raise HTTPException(status_code=400, detail=f"Unknown operation: {str(e)}")
309+
except Exception:
310+
logger.exception("Unhandled error in ripng_batch_configure")
311+
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
@@ -38,6 +38,7 @@
3838
from .pki import PKIBatchBuilder
3939
from .tunnel import TunnelBatchBuilder
4040
from .rip import RipBatchBuilder
41+
from .ripng import RipNgBatchBuilder
4142

4243
# Directly use the self-contained builders
4344
EthernetBatchBuilder = EthernetInterfaceBuilderMixin
@@ -97,6 +98,7 @@
9798
"PKIBatchBuilder",
9899
"TunnelBatchBuilder",
99100
"RipBatchBuilder",
101+
"RipNgBatchBuilder",
100102
"BondingBatchBuilder",
101103
"GeneveBatchBuilder",
102104
"InputBatchBuilder",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""RIPng Protocol Builder Package."""
2+
from .ripng_batch_builder import RipNgBatchBuilder
3+
4+
__all__ = ["RipNgBatchBuilder"]

0 commit comments

Comments
 (0)