Skip to content

Commit 93572aa

Browse files
authored
Allow Peer Endpoint to check Interface for VRF validation (#301)
1 parent 108a09a commit 93572aa

File tree

3 files changed

+143
-45
lines changed

3 files changed

+143
-45
lines changed

changes/301.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Updated PeerEndpoint VRF validation to include related interface as a source for VRF configuration.

nautobot_bgp_models/models.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -627,10 +627,17 @@ def clean(self):
627627
# Enforce peer group VRF membership
628628
if self.peer_group is not None:
629629
if self.peer_group.vrf and (self.peer_group.vrf not in local_ip_value.parent.vrfs.all()):
630-
raise ValidationError(
631-
f"VRF mismatch between {local_ip_value} (VRF {local_ip_value.parent.vrfs.all().first()}) "
632-
f"and peer-group {self.peer_group.name} (VRF {self.peer_group.vrf})"
633-
)
630+
source_interface = self.source_interface or self.peer_group.source_interface
631+
if not source_interface:
632+
raise ValidationError(
633+
f"VRF mismatch between {local_ip_value} (VRF {local_ip_value.parent.vrfs.all().first()}) "
634+
f"and peer-group {self.peer_group.name} (VRF {self.peer_group.vrf})"
635+
)
636+
if source_interface.vrf != self.peer_group.vrf:
637+
raise ValidationError(
638+
f"VRF mismatch between {source_interface} (VRF {source_interface.vrf}) "
639+
f"and peer-group {self.peer_group.name} (VRF {self.peer_group.vrf})"
640+
)
634641

635642

636643
@extras_features(

nautobot_bgp_models/tests/test_models.py

Lines changed: 131 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def test_str(self):
180180
# self.assertEqual(self.peergroup.vrf, vrf)
181181

182182

183-
class PeerEndpointTestCase(TestCase):
183+
class PeerEndpointTestCase(TestCase): # pylint: disable=too-many-instance-attributes
184184
"""Test the PeerEndpoint model."""
185185

186186
@classmethod
@@ -197,15 +197,14 @@ def setUpTestData(cls): # pylint: disable=too-many-locals
197197
location = Location.objects.create(name="Site 1", location_type=location_type, status=location_status)
198198
devicerole = Role.objects.create(name="Router", color="ff0000")
199199
devicerole.content_types.add(ContentType.objects.get_for_model(Device))
200-
device_1 = Device.objects.create(
200+
cls.device_1 = Device.objects.create(
201201
device_type=devicetype, role=devicerole, name="Device 1", location=location, status=cls.status_active
202202
)
203-
interface_status = Status.objects.get_for_model(Interface).first()
204-
cls.interface_1 = Interface.objects.create(device=device_1, name="Loopback1", status=interface_status)
203+
cls.interface_status = Status.objects.get_for_model(Interface).first()
205204
device_2 = Device.objects.create(
206205
device_type=devicetype, role=devicerole, name="Device 2", location=location, status=cls.status_active
207206
)
208-
cls.interface_2 = Interface.objects.create(device=device_2, name="Loopback1", status=interface_status)
207+
cls.interface_2 = Interface.objects.create(device=device_2, name="Loopback1", status=cls.interface_status)
209208

210209
cls.peeringrole_internal = Role.objects.create(name="Internal", color="333333")
211210
cls.peeringrole_internal.content_types.add(ContentType.objects.get_for_model(models.PeerGroup))
@@ -225,7 +224,7 @@ def setUpTestData(cls): # pylint: disable=too-many-locals
225224
bgp_routing_instance_1 = models.BGPRoutingInstance.objects.create(
226225
description="BGP Routing Instance for device 1",
227226
autonomous_system=autonomous_system_12345,
228-
device=device_1,
227+
device=cls.device_1,
229228
status=status_active,
230229
)
231230
cls.bgp_routing_instance_1 = bgp_routing_instance_1
@@ -238,46 +237,59 @@ def setUpTestData(cls): # pylint: disable=too-many-locals
238237
)
239238
cls.bgp_routing_instance_2 = bgp_routing_instance_2
240239

241-
cls.peergroup_1 = models.PeerGroup.objects.create(
242-
name="Peer Group A",
243-
role=cls.peeringrole_internal,
244-
routing_instance=bgp_routing_instance_1,
245-
)
246240
cls.peergroup_2 = models.PeerGroup.objects.create(
247241
name="Peer Group A",
248242
role=cls.peeringrole_internal,
249243
routing_instance=bgp_routing_instance_2,
250244
)
251245

252246
cls.namespace = Namespace.objects.first()
253-
prefix_status = Status.objects.get_for_model(Prefix).first()
254-
Prefix.objects.create(prefix="1.0.0.0/8", namespace=cls.namespace, status=prefix_status)
247+
cls.prefix_status = Status.objects.get_for_model(Prefix).first()
255248

256-
cls.ipaddress_2 = IPAddress.objects.create(
257-
address="1.1.1.2/32",
258-
status=status_active,
259-
namespace=cls.namespace,
260-
)
261249
vm_cluster_type = ClusterType.objects.create(name="vShpere")
262250
vm_cluster = Cluster.objects.create(name="VM Test Cluster", cluster_type=vm_cluster_type)
263251
cls.virtual_machine_1 = VirtualMachine.objects.create(name="VM 1", cluster=vm_cluster, status=cls.status_active)
264252
cls.vm_interface_1 = VMInterface.objects.create(
265253
name="Loopback1", virtual_machine=cls.virtual_machine_1, status=status_active
266254
)
255+
cls.vrf = VRF.objects.create(name="VRF")
267256

268257
def setUp(self):
269258
"""Per-test data setup."""
270259

271260
self.peering = models.Peering.objects.create(status=self.status_active)
272261

262+
self.interface_1 = Interface.objects.create(
263+
device=self.device_1,
264+
name="Loopback1",
265+
status=self.interface_status,
266+
)
267+
268+
self.prefix_1 = Prefix.objects.create(
269+
prefix="1.0.0.0/8",
270+
namespace=self.namespace,
271+
status=self.prefix_status,
272+
)
273+
273274
self.ipaddress_1 = IPAddress.objects.create(
274275
address="1.1.1.1/32",
275276
status=self.status_active,
276277
namespace=self.namespace,
277278
)
279+
self.ipaddress_2 = IPAddress.objects.create(
280+
address="1.1.1.2/32",
281+
status=self.status_active,
282+
namespace=self.namespace,
283+
)
278284

279285
self.interface_1.add_ip_addresses(self.ipaddress_1)
280286

287+
self.peergroup_1 = models.PeerGroup.objects.create(
288+
name="Peer Group A",
289+
role=self.peeringrole_internal,
290+
routing_instance=self.bgp_routing_instance_1,
291+
)
292+
281293
self.peerendpoint_1 = models.PeerEndpoint.objects.create(
282294
source_ip=self.ipaddress_1,
283295
peer_group=self.peergroup_1,
@@ -303,29 +315,107 @@ def test_str(self):
303315
#
304316
# # TODO VRF fixup from router_id?
305317
#
306-
# def test_local_ip_vrf_mismatch(self):
307-
# """Clean should fail if local_ip is assigned to a different VRF than the specified one."""
308-
# self.peerendpoint_1.vrf = VRF.objects.create(name="Some other VRF")
309-
# with self.assertRaises(ValidationError) as context:
310-
# self.peerendpoint_1.validated_save()
311-
# self.assertEqual(
312-
# context.exception.messages[0],
313-
# "VRF Some other VRF was specified, but one or more attributes refer instead to Ark B",
314-
# )
315-
#
316-
# def test_peer_group_vrf_mismatch(self):
317-
# """The specified peer-group must belong to the specified VRF if any."""
318-
# self.peerendpoint_1.peer_group = models.PeerGroup.objects.create(
319-
# name="Group B",
320-
# role=self.peeringrole_internal,
321-
# vrf=None,
322-
# )
323-
# with self.assertRaises(ValidationError) as context:
324-
# self.peerendpoint_1.validated_save()
325-
# self.assertIn(
326-
# "Various attributes refer to different VRFs",
327-
# context.exception.messages[0],
328-
# )
318+
319+
def test_peer_group_no_vrf(self):
320+
"""Test endpoint VRF passes when peer-group doesn't have VRF assigned."""
321+
self.assertIsNone(self.peergroup_1.vrf)
322+
323+
self.peerendpoint_1.peer_group = self.peergroup_1
324+
self.peerendpoint_1.validated_save()
325+
326+
def test_peer_group_vrf_ip_match(self):
327+
"""Test peer-group IP VRF countst for matching VRF."""
328+
self.assertIsNone(self.interface_1.vrf)
329+
330+
self.peergroup_1.vrf = self.vrf
331+
self.peergroup_1.save()
332+
self.prefix_1.vrfs.add(self.vrf)
333+
self.prefix_1.save()
334+
335+
self.peerendpoint_1.peer_group = self.peergroup_1
336+
self.peerendpoint_1.validated_save()
337+
338+
def test_peer_group_vrf_ip_mismatch(self):
339+
"""Test endpoint VRF mismatch raises error when no interface defined."""
340+
self.assertIsNone(self.peerendpoint_1.source_interface)
341+
self.assertIsNone(self.peergroup_1.source_interface)
342+
self.assertTrue(self.vrf not in self.prefix_1.vrfs.all())
343+
344+
self.peergroup_1.vrf = self.vrf
345+
self.peergroup_1.save()
346+
347+
self.peerendpoint_1.peer_group = self.peergroup_1
348+
349+
with self.assertRaises(ValidationError) as context:
350+
self.peerendpoint_1.validated_save()
351+
352+
self.assertEqual(
353+
context.exception.messages[0],
354+
f"VRF mismatch between {self.ipaddress_1} (VRF None) "
355+
f"and peer-group {self.peergroup_1.name} (VRF {self.peergroup_1.vrf})",
356+
)
357+
358+
def test_vrf_interface_match(self):
359+
"""Test endpoint interface VRF counts for matching VRF."""
360+
self.assertIsNone(self.peergroup_1.source_interface)
361+
362+
self.interface_1.vrf = self.vrf
363+
self.interface_1.save()
364+
self.peergroup_1.vrf = self.vrf
365+
self.peergroup_1.save()
366+
self.peerendpoint_1.source_interface = self.interface_1
367+
self.peerendpoint_1.peer_group = self.peergroup_1
368+
self.peerendpoint_1.validated_save()
369+
370+
def test_vrf_interface_mismatch(self):
371+
"""Test endpoint interface VRF mismatch raises error."""
372+
self.assertIsNone(self.peergroup_1.source_interface)
373+
self.assertIsNone(self.interface_1.vrf)
374+
375+
self.peergroup_1.vrf = self.vrf
376+
self.peergroup_1.save()
377+
self.peerendpoint_1.source_interface = self.interface_1
378+
self.peerendpoint_1.peer_group = self.peergroup_1
379+
380+
with self.assertRaises(ValidationError) as context:
381+
self.peerendpoint_1.validated_save()
382+
383+
self.assertEqual(
384+
context.exception.messages[0],
385+
f"VRF mismatch between {self.interface_1} (VRF None) "
386+
f"and peer-group {self.peergroup_1.name} (VRF {self.peergroup_1.vrf})",
387+
)
388+
389+
def test_peer_group_vrf_interface_match(self):
390+
"""Test peer-group interface VRF counts for matching VRF."""
391+
self.assertIsNone(self.peerendpoint_1.source_interface)
392+
393+
self.interface_1.vrf = self.vrf
394+
self.interface_1.save()
395+
self.peergroup_1.vrf = self.vrf
396+
self.peergroup_1.source_interface = self.interface_1
397+
self.peergroup_1.save()
398+
self.peerendpoint_1.peer_group = self.peergroup_1
399+
self.peerendpoint_1.validated_save()
400+
401+
def test_peer_group_vrf_interface_mismatch(self):
402+
"""Test peer-group interface VRF mismatch raises error."""
403+
self.assertIsNone(self.peerendpoint_1.source_interface)
404+
self.assertIsNone(self.interface_1.vrf)
405+
406+
self.peergroup_1.source_interface = self.interface_1
407+
self.peergroup_1.vrf = self.vrf
408+
self.peergroup_1.save()
409+
self.peerendpoint_1.peer_group = self.peergroup_1
410+
411+
with self.assertRaises(ValidationError) as context:
412+
self.peerendpoint_1.validated_save()
413+
414+
self.assertEqual(
415+
context.exception.messages[0],
416+
f"VRF mismatch between {self.interface_1} (VRF None) "
417+
f"and peer-group {self.peergroup_1.name} (VRF {self.peergroup_1.vrf})",
418+
)
329419

330420
def test_deleting_ip_address_protects_endpoint(self):
331421
"""Deleting an IPAddress should protect the associated PeerEndpoint(s)."""

0 commit comments

Comments
 (0)