Skip to content

Commit 4a1a17e

Browse files
authored
Merge pull request #53 from wobcom/add-evpn-vpws-support
EVPN-VPWS support + account for L2VPN identifiers being optional
2 parents aaf5db9 + f7649d2 commit 4a1a17e

File tree

5 files changed

+88
-22
lines changed

5 files changed

+88
-22
lines changed

cosmo/l2vpnhelpertypes.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ def isValidNumberOfTerminations(self, i: int):
102102
def getInvalidNumberOfTerminationsErrorMessage(cls, i: int):
103103
return f"{cls.getNetboxTypeName().upper()}: {i} is not a valid number of terminations, ignoring..."
104104

105+
def needsL2VPNIdentifierAsMandatory(self) -> bool:
106+
return False
107+
105108
def getChosenEncapType(self, o: AbstractNetboxType) -> str | None:
106109
chosen_encap = head(list(filter(
107110
lambda encap: encap is not None,
@@ -263,22 +266,21 @@ def getAcceptedTerminationTypes() -> T:
263266
return InterfaceType
264267

265268

266-
class VPWSL2VpnTypeTerminationVisitor(AbstractP2PL2VpnTypeTerminationVisitor):
269+
class AbstractVPWSEVPNVPWSVpnTypeCommon(AbstractL2VpnTypeTerminationVisitor, metaclass=ABCMeta):
267270
@staticmethod
268271
def getSupportedEncapTraits() -> list[type[AbstractEncapCapability]]:
269272
return [
270273
VlanCccEncapCapability,
271274
EthernetCccEncapCapability,
272275
]
273276

274-
@staticmethod
275-
def getNetboxTypeName() -> str:
276-
return "vpws"
277-
278277
@staticmethod
279278
def getAcceptedTerminationTypes() -> T:
280279
return InterfaceType
281280

281+
def needsL2VPNIdentifierAsMandatory(self) -> bool:
282+
return True
283+
282284
def processInterfaceTypeTermination(self, o: InterfaceType) -> dict | None:
283285
parent_l2vpn = o.getParent(L2VPNType)
284286
# find local end
@@ -319,6 +321,18 @@ def processInterfaceTypeTermination(self, o: InterfaceType) -> dict | None:
319321
} | self.spitInterfaceEncapFor(o)
320322

321323

324+
class VPWSL2VpnTypeTerminationVisitor(AbstractVPWSEVPNVPWSVpnTypeCommon, AbstractP2PL2VpnTypeTerminationVisitor):
325+
@staticmethod
326+
def getNetboxTypeName() -> str:
327+
return "vpws"
328+
329+
330+
class EVPNVPWSVpnTypeTerminationVisitor(AbstractVPWSEVPNVPWSVpnTypeCommon, AbstractP2PL2VpnTypeTerminationVisitor):
331+
@staticmethod
332+
def getNetboxTypeName() -> str:
333+
return "evpn-vpws"
334+
335+
322336
class VXLANEVPNL2VpnTypeTerminationVisitor(AbstractAnyToAnyL2VpnTypeTerminationVisitor):
323337
@staticmethod
324338
def getSupportedEncapTraits() -> list[type[AbstractEncapCapability]]:
@@ -332,6 +346,9 @@ def getNetboxTypeName() -> str:
332346
def getAcceptedTerminationTypes() -> T:
333347
return VLANType
334348

349+
def needsL2VPNIdentifierAsMandatory(self) -> bool:
350+
return True
351+
335352
def processVLANTypeTermination(self, o: VLANType) -> dict | None:
336353
def spitNameForInterfaces(int_or_vlan: InterfaceType | VLANType):
337354
if isinstance(int_or_vlan, InterfaceType):
@@ -383,6 +400,9 @@ def getNetboxTypeName() -> str:
383400
def getAcceptedTerminationTypes() -> T:
384401
return InterfaceType, VLANType
385402

403+
def needsL2VPNIdentifierAsMandatory(self) -> bool:
404+
return True
405+
386406
def processTerminationCommon(self, o: InterfaceType|VLANType) -> dict|None:
387407
parent_l2vpn = o.getParent(L2VPNType)
388408
interface_names = None
@@ -434,6 +454,7 @@ class L2VpnVisitorClassFactoryFromL2VpnTypeObject:
434454
MPLSEVPNL2VpnTypeTerminationVisitor,
435455
VXLANEVPNL2VpnTypeTerminationVisitor,
436456
VPWSL2VpnTypeTerminationVisitor,
457+
EVPNVPWSVpnTypeTerminationVisitor,
437458
EVPLL2VpnTypeTerminationVisitorAbstract,
438459
EPLL2VpnTypeTerminationVisitorAbstract,
439460
)

cosmo/routerl2vpnvisitor.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from functools import singledispatchmethod
44

55
from cosmo.abstractroutervisitor import AbstractRouterExporterVisitor
6-
from cosmo.common import head
6+
from cosmo.common import head, L2VPNSerializationError
77
from cosmo.l2vpnhelpertypes import L2VpnVisitorClassFactoryFromL2VpnTypeObject, AbstractL2VpnTypeTerminationVisitor
88
from cosmo.types import L2VPNType, InterfaceType, VLANType, CosmoLoopbackType, L2VPNTerminationType, DeviceType
99

@@ -29,18 +29,24 @@ class RouterL2VPNValidatorVisitor(AbstractL2VPNVisitor):
2929
def accept(self, o):
3030
return super().accept(o)
3131

32-
def isCompliantWANL2VPN(self, o: L2VPNType) -> bool:
32+
def isCompliantWANL2VPN(self, o: L2VPNType):
3333
terminations = o.getTerminations()
34+
identifier = o.getIdentifier()
3435
l2vpn_type = self.getL2VpnTypeTerminationObjectFrom(o)
36+
if l2vpn_type.needsL2VPNIdentifierAsMandatory() and identifier is None:
37+
raise L2VPNSerializationError(
38+
f"for {o.getName()}: L2VPN identifier is mandatory."
39+
)
3540
if not l2vpn_type.isValidNumberOfTerminations(len(terminations)):
36-
warnings.warn(f"for {o.getName()}: "
37-
f"{l2vpn_type.getInvalidNumberOfTerminationsErrorMessage(len(terminations))}")
38-
return False
41+
raise L2VPNSerializationError(
42+
f"for {o.getName()}: "
43+
f"{l2vpn_type.getInvalidNumberOfTerminationsErrorMessage(len(terminations))}"
44+
)
3945
if any([not isinstance(t, l2vpn_type.getAcceptedTerminationTypes()) for t in terminations]):
40-
warnings.warn(f"Found unsupported L2VPN termination in \"{o.getName()}\". "
41-
f"Accepted types are: {l2vpn_type.getAcceptedTerminationTypes()}")
42-
return False
43-
return True
46+
raise L2VPNSerializationError(
47+
f"Found unsupported L2VPN termination in \"{o.getName()}\". "
48+
f"Accepted types are: {l2vpn_type.getAcceptedTerminationTypes()}"
49+
)
4450

4551
@accept.register
4652
def _(self, o: L2VPNType):

cosmo/tests/test_case_local_l2x.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ device_list:
102102
l2vpn_list:
103103
- id: '53'
104104
__typename: L2VPNType
105-
identifier: null
105+
identifier: 12345
106106
name: 'WAN: L2X'
107107
terminations:
108108
- assigned_object:

cosmo/tests/test_serializer.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import yaml
22
import pytest
33
import copy
4+
5+
from cosmo.common import L2VPNSerializationError
46
from coverage.html import os
57

68
from cosmo.serializer import RouterSerializer, SwitchSerializer
@@ -64,7 +66,7 @@ def test_l2vpn_errors():
6466
vpws_incorrect_terminations['l2vpn_list'].append({
6567
'__typename': 'L2VPNType',
6668
'id': '53',
67-
'identifier': None,
69+
'identifier': 123456,
6870
'name': 'WAN: incorrect VPWS',
6971
'type': 'VPWS',
7072
'terminations': [
@@ -78,14 +80,17 @@ def test_l2vpn_errors():
7880
'__typename': 'L2VPNTerminationType',
7981
'assigned_object': {}
8082
}]})
81-
with pytest.warns(UserWarning, match="VPWS circuits are only allowed to have 2 terminations"):
83+
with pytest.raises(
84+
L2VPNSerializationError,
85+
match="VPWS circuits are only allowed to have 2 terminations"
86+
):
8287
serialize(vpws_incorrect_terminations)
8388

8489
unsupported_type_terminations = copy.deepcopy(template)
8590
unsupported_type_terminations['l2vpn_list'].append({
8691
'__typename': 'L2VPNType',
8792
'id': '54',
88-
'identifier': None,
93+
'identifier': 123456,
8994
'name': 'WAN: unsupported termination types 1',
9095
'type': 'VPWS',
9196
'terminations': [
@@ -97,14 +102,17 @@ def test_l2vpn_errors():
97102
'__typename': 'L2VPNTerminationType',
98103
'assigned_object': {}
99104
}]})
100-
with pytest.warns(UserWarning, match=r"VPWS L2VPN does not support|Found unsupported L2VPN termination in"):
105+
with pytest.raises(
106+
L2VPNSerializationError,
107+
match=r"VPWS L2VPN does not support|Found unsupported L2VPN termination in"
108+
):
101109
serialize(unsupported_type_terminations)
102110

103111
vpws_non_interface_term = copy.deepcopy(template)
104112
vpws_non_interface_term['l2vpn_list'].append({
105113
'__typename': 'L2VPNType',
106114
'id': '54',
107-
'identifier': None,
115+
'identifier': 123456,
108116
'name': 'WAN: WAN: unsupported termination types 2',
109117
'type': 'VPWS',
110118
'terminations': [
@@ -118,9 +126,40 @@ def test_l2vpn_errors():
118126
'assigned_object': {
119127
'__typename': "VLANType"
120128
}}]})
121-
with pytest.warns(UserWarning, match=r"VPWS L2VPN does not support|Found unsupported L2VPN termination in"):
129+
with pytest.raises(
130+
L2VPNSerializationError,
131+
match=r"VPWS L2VPN does not support|Found unsupported L2VPN termination in"
132+
):
122133
serialize(vpws_non_interface_term)
123134

135+
vpws_missing_identifier = copy.deepcopy(template)
136+
vpws_missing_identifier['l2vpn_list'].append({
137+
'__typename': 'L2VPNType',
138+
'id': '54',
139+
'identifier': None,
140+
'name': 'WAN: WAN: missing L2VPN identifier',
141+
'type': 'evpn-vpws',
142+
'terminations': [
143+
{
144+
'__typename': 'L2VPNTerminationType',
145+
'assigned_object': {
146+
'__typename': "InterfaceType"
147+
}
148+
},
149+
{
150+
'__typename': 'L2VPNTerminationType',
151+
'assigned_object': {
152+
'__typename': "InterfaceType"
153+
}
154+
}
155+
]
156+
})
157+
with pytest.raises(
158+
L2VPNSerializationError,
159+
match=r"L2VPN identifier is mandatory.",
160+
):
161+
serialize(vpws_missing_identifier)
162+
124163

125164
def test_router_physical_interface():
126165
[sd] = get_router_sd_from_path("./test_case_1.yaml")

cosmo/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ class L2VPNType(AbstractNetboxType):
398398
def __repr__(self):
399399
return f"{super().__repr__()}({self.getName()})"
400400

401-
def getIdentifier(self):
401+
def getIdentifier(self) -> int|None:
402402
return self["identifier"]
403403

404404
def getType(self) -> str:

0 commit comments

Comments
 (0)