Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

# Import routers
from routers.session import session as session_router
from routers.interfaces import ethernet, dummy, bonding, bridge, geneve, input, l2tpv3, loopback, macsec, openvpn, pppoe, pseudo_ethernet, sstpc, virtual_ethernet
from routers.interfaces import ethernet, dummy, bonding, bridge, geneve, input, l2tpv3, loopback, macsec, openvpn, pppoe, pseudo_ethernet, sstpc, virtual_ethernet, vpp
from routers.firewall import groups
from routers.firewall import ipv4 as firewall_ipv4
from routers.firewall import ipv6 as firewall_ipv6
Expand Down Expand Up @@ -294,6 +294,7 @@ async def get_permissions(request: Request) -> dict:
app.include_router(pseudo_ethernet.router)
app.include_router(sstpc.router)
app.include_router(virtual_ethernet.router)
app.include_router(vpp.router)
app.include_router(groups.router)
app.include_router(firewall_ipv4.router)
app.include_router(firewall_ipv6.router)
Expand Down Expand Up @@ -370,6 +371,7 @@ async def read_root() -> dict:
"pseudo-ethernet-interface",
"sstpc-interface",
"virtual-ethernet-interface",
"vpp-interface",
"firewall-groups",
"firewall-ipv4",
"firewall-ipv6",
Expand Down
266 changes: 266 additions & 0 deletions backend/routers/interfaces/vpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
"""
VPP Interface Configuration Endpoints

All VPP (Vector Packet Processing) interface endpoints for VyOS 1.5+.
VPP supports seven interface types under `interfaces vpp`:
bonding (vppbondN), bridge (vppbrN), gre (vppgreN), ipip (vppipipN),
loopback (vpploN), vxlan (vppvxlanN), xconnect (vppxconN)

Multi-parameter builder operations encode extra parameters as colon-separated
values in the `value` field, e.g. "vlan_id:address" for vif address ops.
"""

import inspect
import logging
from typing import Dict, List, Optional, Any

from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
from starlette.concurrency import run_in_threadpool

from fastapi_permissions import require_read_permission, require_write_permission
from rbac_permissions import FeatureGroup
from session_vyos_service import get_session_vyos_service

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/vyos/vpp", tags=["vpp-interface"])


# =============================================================================
# Request / Response Models
# =============================================================================


class BatchOperation(BaseModel):
op: str = Field(..., description="Builder method name")
value: Optional[str] = Field(None, description="Value (colon-separated for multi-param ops)")


class BatchRequest(BaseModel):
interface: str = Field(..., description="Interface name (e.g., vppbond0, vppgre1)")
operations: List[BatchOperation]


class VyOSResponse(BaseModel):
success: bool
data: Optional[Dict[str, Any]] = None
error: Optional[str] = None


# ---- Per-type config models -------------------------------------------------


class VifConfig(BaseModel):
vlan_id: str
description: Optional[str] = None
disabled: bool = False
addresses: List[str] = Field(default_factory=list)
mtu: Optional[str] = None


class BridgeMember(BaseModel):
interface: str
bvi: bool = False


class BondingConfig(BaseModel):
name: str
description: Optional[str] = None
disabled: bool = False
mode: Optional[str] = None
hash_policy: Optional[str] = None
mac: Optional[str] = None
mtu: Optional[str] = None
addresses: List[str] = Field(default_factory=list)
members: List[str] = Field(default_factory=list)
vif: List[VifConfig] = Field(default_factory=list)


class BridgeConfig(BaseModel):
name: str
description: Optional[str] = None
members: List[BridgeMember] = Field(default_factory=list)


class GreConfig(BaseModel):
name: str
description: Optional[str] = None
disabled: bool = False
addresses: List[str] = Field(default_factory=list)
mtu: Optional[str] = None
remote: Optional[str] = None
source_address: Optional[str] = None
tunnel_type: Optional[str] = None
key: Optional[str] = None


class IpipConfig(BaseModel):
name: str
description: Optional[str] = None
disabled: bool = False
addresses: List[str] = Field(default_factory=list)
mtu: Optional[str] = None
remote: Optional[str] = None
source_address: Optional[str] = None


class LoopbackConfig(BaseModel):
name: str
description: Optional[str] = None
disabled: bool = False
addresses: List[str] = Field(default_factory=list)
mtu: Optional[str] = None
vif: List[VifConfig] = Field(default_factory=list)


class VxlanConfig(BaseModel):
name: str
description: Optional[str] = None
disabled: bool = False
addresses: List[str] = Field(default_factory=list)
mtu: Optional[str] = None
remote: Optional[str] = None
source_address: Optional[str] = None
vni: Optional[str] = None


class XconnectConfig(BaseModel):
name: str
description: Optional[str] = None
disabled: bool = False
members: List[str] = Field(default_factory=list)


class VppConfigResponse(BaseModel):
bonding: List[BondingConfig] = Field(default_factory=list)
bridge: List[BridgeConfig] = Field(default_factory=list)
gre: List[GreConfig] = Field(default_factory=list)
ipip: List[IpipConfig] = Field(default_factory=list)
loopback: List[LoopbackConfig] = Field(default_factory=list)
vxlan: List[VxlanConfig] = Field(default_factory=list)
xconnect: List[XconnectConfig] = Field(default_factory=list)
total: int = 0


# =============================================================================
# Endpoints
# =============================================================================


@router.get("/capabilities")
async def get_capabilities(request: Request) -> Dict[str, Any]:
"""Return version-aware feature capabilities for VPP interfaces."""
await require_read_permission(request, FeatureGroup.INTERFACES)
service = get_session_vyos_service(request)
from vyos_builders.interfaces.vpp import VppInterfaceBuilderMixin
builder = VppInterfaceBuilderMixin(version=service.get_version())
return builder.get_capabilities()


@router.get("/config", response_model=VppConfigResponse)
async def get_config(http_request: Request, refresh: bool = False) -> VppConfigResponse:
"""Get all VPP interface configurations from VyOS."""
await require_read_permission(http_request, FeatureGroup.INTERFACES)
try:
service = get_session_vyos_service(http_request)
full_config = await run_in_threadpool(service.get_full_config, refresh)
raw_config = full_config.get("interfaces", {}).get("vpp", {})

from vyos_mappers.interfaces.vpp_versions import get_vpp_mapper
mapper = get_vpp_mapper(service.get_version())
parsed = mapper.parse_all_vpp_interfaces(raw_config)
return VppConfigResponse(**parsed)
except Exception:
logger.exception("Unhandled error in get_config")
raise HTTPException(status_code=500, detail="Internal server error")


@router.post("/batch", response_model=VyOSResponse)
async def batch_configure(http_request: Request, request: BatchRequest) -> VyOSResponse:
"""
Configure a VPP interface using batch operations.

Operation names match builder methods exactly, e.g.:
set_bonding_description, set_gre_remote, delete_vxlan_vni

Multi-parameter operations use colon-separated `value`:
set_bonding_vif_address → value="100:192.0.2.1/24" (vlan_id:address)
set_bonding_vif_mtu → value="100:1500" (vlan_id:mtu)
set_loopback_vif_address → value="10:10.0.0.1/24" (vlan_id:address)
"""
await require_write_permission(http_request, FeatureGroup.INTERFACES)

try:
service = get_session_vyos_service(http_request)
batch = service.create_vpp_batch()

for op in request.operations:
if op.op in batch._INTERNAL_BUILDER_METHODS:
raise HTTPException(
status_code=400,
detail=f"Operation '{op.op}' is not a valid interface operation",
)

method = getattr(batch, op.op, None)
if method is None:
raise HTTPException(
status_code=400,
detail=f"Unsupported operation: {op.op}",
)

sig = inspect.signature(method)
params = [p for p in sig.parameters.keys() if p != "self"]

if len(params) == 1:
method(request.interface)
elif len(params) == 2:
if op.value is None:
raise HTTPException(
status_code=400,
detail=f"Operation '{op.op}' requires a value",
)
method(request.interface, op.value)
elif len(params) == 3:
if op.value is None:
raise HTTPException(
status_code=400,
detail=f"Operation '{op.op}' requires a value in 'param1:param2' format",
)
parts = op.value.split(":", 1)
if len(parts) != 2:
raise HTTPException(
status_code=400,
detail=f"Operation '{op.op}' requires value in 'param1:param2' format",
)
method(request.interface, parts[0], parts[1])
elif len(params) == 4:
if op.value is None:
raise HTTPException(
status_code=400,
detail=f"Operation '{op.op}' requires a value in 'param1:param2:param3' format",
)
parts = op.value.split(":", 2)
if len(parts) != 3:
raise HTTPException(
status_code=400,
detail=f"Operation '{op.op}' requires value in 'param1:param2:param3' format",
)
method(request.interface, parts[0], parts[1], parts[2])
else:
raise HTTPException(
status_code=400,
detail=f"Operation '{op.op}' has unexpected signature",
)

response = service.execute_batch(batch)
return VyOSResponse(
success=response.status == 200,
data=response.result if isinstance(response.result, dict) else None,
error=response.error if response.error else None,
)
except HTTPException:
raise
except Exception:
logger.exception("Unhandled error in batch_configure")
raise HTTPException(status_code=500, detail="Internal server error")
4 changes: 3 additions & 1 deletion backend/vyos_builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Each builder includes all necessary operations for its feature type.
"""

from .interfaces import EthernetInterfaceBuilderMixin, DummyInterfaceBuilderMixin, BondingInterfaceBuilderMixin, GeneveInterfaceBuilderMixin, InputInterfaceBuilderMixin, L2TPv3InterfaceBuilderMixin, LoopbackInterfaceBuilderMixin, MacsecInterfaceBuilderMixin, OpenvpnInterfaceBuilderMixin, PppoeInterfaceBuilderMixin, PseudoEthernetInterfaceBuilderMixin, SstpcInterfaceBuilderMixin, VirtualEthernetInterfaceBuilderMixin
from .interfaces import EthernetInterfaceBuilderMixin, DummyInterfaceBuilderMixin, BondingInterfaceBuilderMixin, GeneveInterfaceBuilderMixin, InputInterfaceBuilderMixin, L2TPv3InterfaceBuilderMixin, LoopbackInterfaceBuilderMixin, MacsecInterfaceBuilderMixin, OpenvpnInterfaceBuilderMixin, PppoeInterfaceBuilderMixin, PseudoEthernetInterfaceBuilderMixin, SstpcInterfaceBuilderMixin, VirtualEthernetInterfaceBuilderMixin, VppInterfaceBuilderMixin
from .firewall import FirewallGroupsBatchBuilder, FirewallIPv4BatchBuilder, FirewallIPv6BatchBuilder, BridgeFirewallBatchBuilder, FirewallZonesBatchBuilder
from .nat import NATBatchBuilder
from .nat64 import NAT64BatchBuilder
Expand Down Expand Up @@ -52,6 +52,7 @@
PseudoEthernetBatchBuilder = PseudoEthernetInterfaceBuilderMixin
SstpcBatchBuilder = SstpcInterfaceBuilderMixin
VirtualEthernetBatchBuilder = VirtualEthernetInterfaceBuilderMixin
VppBatchBuilder = VppInterfaceBuilderMixin

__all__ = [
"EthernetBatchBuilder",
Expand Down Expand Up @@ -103,4 +104,5 @@
"PseudoEthernetBatchBuilder",
"SstpcBatchBuilder",
"VirtualEthernetBatchBuilder",
"VppBatchBuilder",
]
2 changes: 2 additions & 0 deletions backend/vyos_builders/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .pseudo_ethernet import PseudoEthernetInterfaceBuilderMixin
from .sstpc import SstpcInterfaceBuilderMixin
from .virtual_ethernet import VirtualEthernetInterfaceBuilderMixin
from .vpp import VppInterfaceBuilderMixin

__all__ = [
"EthernetInterfaceBuilderMixin",
Expand All @@ -34,4 +35,5 @@
"PseudoEthernetInterfaceBuilderMixin",
"SstpcInterfaceBuilderMixin",
"VirtualEthernetInterfaceBuilderMixin",
"VppInterfaceBuilderMixin",
]
Loading
Loading