Skip to content

Commit 393d9b3

Browse files
committed
L2VPN/VPWS: derive route distinguisher and route target
1 parent 0091fe7 commit 393d9b3

15 files changed

+73
-62
lines changed

cosmo.example.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
fqdnSuffix: infra.example.com
2-
2+
asn: 65542
33
devices:
44
router:
55
- "router1"

cosmo/__main__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def main() -> int:
4949
with open(args.config, 'r') as cfg_file:
5050
cosmo_configuration = yaml.safe_load(cfg_file)
5151

52+
if not 'asn' in cosmo_configuration:
53+
error(f"Field 'asn' not defined in configuration file", None)
54+
return 1
55+
5256
info(f"Fetching information from Netbox, make sure VPN is enabled on your system.")
5357

5458
netbox_url = os.environ.get("NETBOX_URL")
@@ -83,7 +87,7 @@ def noop(*args, **kwargs):
8387
content = None
8488
try:
8589
if device['name'] in cosmo_configuration['devices']['router']:
86-
router_serializer = RouterSerializer(device, cosmo_data['l2vpn_list'], cosmo_data["loopbacks"])
90+
router_serializer = RouterSerializer(device, cosmo_data['l2vpn_list'], cosmo_data["loopbacks"], cosmo_configuration["asn"])
8791
content = router_serializer.serialize()
8892
elif device['name'] in cosmo_configuration['devices']['switch']:
8993
switch_serializer = SwitchSerializer(device)

cosmo/l2vpnhelpertypes.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def __init__(self, *args, associated_l2vpn: L2VPNType, loopbacks_by_device: dict
7575
self.associated_l2vpn = associated_l2vpn
7676
self.loopbacks_by_device = loopbacks_by_device
7777
self.asn = asn
78+
# when using 32 bit ASN we have to append L to the route target prefix
79+
self.rt = str(asn) if asn <= 2**16 else f"{asn}L"
7880

7981
def __repr__(self):
8082
return f"{self.__class__.__name__}({self.associated_l2vpn})"
@@ -318,14 +320,15 @@ def processInterfaceTypeTermination(self, o: InterfaceType) -> dict | None:
318320
lambda i: i != local,
319321
parent_l2vpn.getTerminations()
320322
))
323+
router_id = o.getParent(DeviceType).getRouterID()
321324
return {
322325
self._vrf_key: {
323326
parent_l2vpn.getName().replace("WAN: ", ""): {
324327
"interfaces": [o.getName()],
325328
"description": f"VPWS: {parent_l2vpn.getName().replace('WAN: VS_', '')}",
326329
"instance_type": "evpn-vpws",
327-
"route_distinguisher": f"{self.asn}:{str(parent_l2vpn.getIdentifier())}",
328-
"vrf_target": f"target:1:{str(parent_l2vpn.getIdentifier())}",
330+
"route_distinguisher": f"{router_id}:{str(parent_l2vpn.getIdentifier())}",
331+
"vrf_target": f"target:{self.rt}:{str(parent_l2vpn.getIdentifier())}",
329332
"protocols": {
330333
"evpn": {
331334
"interfaces": {

cosmo/netbox_types.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from abc import abstractmethod
88
from ipaddress import IPv4Interface, IPv6Interface
99

10-
from .common import without_keys, JsonOutputType
10+
from .common import without_keys, JsonOutputType, DeviceSerializationError
1111
from typing import Self, Iterator, TypeVar, NoReturn
1212

1313

@@ -221,6 +221,33 @@ def getInterfaces(self) -> list["InterfaceType"]:
221221
def getSerial(self) -> str:
222222
return self.get("serial", "")
223223

224+
def getRouterID(self) -> str:
225+
# Deriving the router ID is a bit tricky, there is no 'correct' way.
226+
# For us it's the primary loopback IPv4 address
227+
228+
# get first loopback interface in default vrf
229+
loopback = next(filter(
230+
lambda x: (x.isLoopbackChild() and x.getVRF() == None),
231+
self.getInterfaces()
232+
), None)
233+
234+
if loopback == None:
235+
raise DeviceSerializationError("Can't derive Router ID, no suitable loopback interface found.")
236+
return ""
237+
238+
# get first IPv4 of that interface
239+
address = next(filter(
240+
lambda i: type(i) is IPv4Interface,
241+
map(lambda i: i.getIPInterfaceObject(), loopback.getIPAddresses())
242+
), None)
243+
244+
if address == None:
245+
raise DeviceSerializationError("Can't derive Router ID, no suitable loopback IP address found.")
246+
return ""
247+
248+
# return that IP without subnet mask and hope for the best
249+
return str(address.ip)
250+
224251

225252
class DeviceTypeType(AbstractNetboxType):
226253
def getBasePath(self):
@@ -351,7 +378,9 @@ def getSubInterfaceParentInterfaceName(self) -> str|None:
351378
return ret
352379

353380
def getVRF(self) -> VRFType|None:
354-
return self.get("vrf")
381+
if self["vrf"]:
382+
return VRFType(self["vrf"])
383+
return None
355384

356385
def spitInterfacePathWith(self, d: dict) -> dict:
357386
"""
@@ -435,6 +464,9 @@ def getIPAddresses(self) -> list[IPAddressType]:
435464
def hasParentInterface(self) -> bool:
436465
return bool(self.get("parent"))
437466

467+
def isLoopbackChild(self):
468+
return '.' in self.getName() and self.getName().startswith("lo")
469+
438470
def getConnectedEndpoints(self) -> list[DeviceType]:
439471
return self.get("connected_endpoints", [])
440472

@@ -475,7 +507,10 @@ def getBasePath(self):
475507
return "/vpn/l2vpns/"
476508

477509
def getIdentifier(self) -> int|None:
478-
return self["identifier"]
510+
if self["identifier"]:
511+
return self["identifier"]
512+
else:
513+
return self["id"]
479514

480515
def getType(self) -> str:
481516
return self["type"]

cosmo/routerl2vpnvisitor.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ def isCompliantWANL2VPN(self, o: L2VPNType):
3232
terminations = o.getTerminations()
3333
identifier = o.getIdentifier()
3434
l2vpn_type = self.getL2VpnTypeTerminationObjectFrom(o)
35-
if l2vpn_type.needsL2VPNIdentifierAsMandatory() and identifier is None:
36-
raise L2VPNSerializationError(
37-
f"for {o.getName()}: L2VPN identifier is mandatory."
38-
)
3935
if not l2vpn_type.isValidNumberOfTerminations(len(terminations)):
4036
raise L2VPNSerializationError(
4137
f"for {o.getName()}: "

cosmo/routervisitor.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,12 @@ def _(self, o: InterfaceType):
260260
}
261261
}
262262

263-
def getRouterId(self, o: DeviceType) -> str:
264-
return str(ipaddress.ip_interface(str(self.loopbacks_by_device[o.getName()].getIpv4())).ip)
265-
266263
@accept.register
267264
def _(self, o: VRFType):
268265
parent_interface = o.getParent(InterfaceType)
269266
if not parent_interface.isSubInterface():
270267
return # guard: do not process root interface
271-
router_id = self.getRouterId(o.getParent(DeviceType))
268+
router_id = o.getParent(DeviceType).getRouterID()
272269
if o.getRouteDistinguisher():
273270
rd = router_id + ":" + o.getRouteDistinguisher()
274271
elif len(o.getExportTargets()):

cosmo/serializer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
class RouterSerializer:
12-
def __init__(self, device, l2vpn_list, loopbacks):
12+
def __init__(self, device, l2vpn_list, loopbacks, asn):
1313
try:
1414
match device["platform"]["manufacturer"]["slug"]:
1515
case 'juniper':
@@ -29,6 +29,7 @@ def __init__(self, device, l2vpn_list, loopbacks):
2929
self.device = device
3030
self.l2vpn_list = l2vpn_list
3131
self.loopbacks = loopbacks
32+
self.asn = asn
3233

3334
self.l2vpns = {}
3435
self.l3vpns = {}
@@ -51,7 +52,7 @@ def serialize(self):
5152
# breakpoint()
5253
visitor = RouterDeviceExporterVisitor(
5354
loopbacks_by_device={k: CosmoLoopbackType(v) for k, v in self.loopbacks.items()},
54-
asn=9136,
55+
asn=self.asn,
5556
)
5657
for value in iter(DeviceType(self.device)):
5758
try:

cosmo/tests/cosmo.devgen_ansible.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
output_format: "ansible"
2-
2+
asn: 65542
33
devices:
44
router:
55
- "TEST0001"

cosmo/tests/cosmo.devgen_nix.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
output_format: "nix"
2-
2+
asn: 65542
33
devices:
44
router:
55
- "TEST0001"

cosmo/tests/test_case_bgpcpe.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ device_list:
120120
__typename: VRFType
121121
description: ''
122122
export_targets:
123-
- name: target:9136:407
123+
- name: target:65542:407
124124
__typename: RouteTargetType
125125
id: '407'
126126
import_targets:
127-
- name: target:9136:407
127+
- name: target:65542:407
128128
__typename: RouteTargetType
129129
name: L3VPN
130130
rd: null
@@ -165,11 +165,11 @@ device_list:
165165
__typename: VRFType
166166
description: ''
167167
export_targets:
168-
- name: target:9136:407
168+
- name: target:65542:407
169169
__typename: RouteTargetType
170170
id: '407'
171171
import_targets:
172-
- name: target:9136:407
172+
- name: target:65542:407
173173
__typename: RouteTargetType
174174
name: L3VPN
175175
rd: null

0 commit comments

Comments
 (0)