unnumbered: Add syntax and semantic checking and unit tests

This commit is contained in:
Pim van Pelt
2024-04-07 14:17:32 +02:00
parent e427d93197
commit 476d909904
9 changed files with 362 additions and 3 deletions

View File

@ -152,6 +152,23 @@ def get_l2xc_interfaces(yaml):
return ret return ret
def get_unnumbered_interfaces(yaml):
"""Returns a list of all interfaces that are unnumbered"""
ret = []
if not "interfaces" in yaml:
return ret
for ifname, iface in yaml["interfaces"].items():
if "unnumbered" in iface:
ret.append(ifname)
if "sub-interfaces" in iface:
for subid, sub_iface in iface["sub-interfaces"].items():
sub_ifname = f"{ifname}.{int(subid)}"
if "unnumbered" in sub_iface:
ret.append(sub_ifname)
return ret
def is_l2xc_interface(yaml, ifname): def is_l2xc_interface(yaml, ifname):
"""Returns True if this interface has an L2 CrossConnect""" """Returns True if this interface has an L2 CrossConnect"""
@ -381,6 +398,11 @@ def is_l3(yaml, ifname):
return not is_l2(yaml, ifname) return not is_l2(yaml, ifname)
def is_unnumbered(yaml, ifname):
"""Returns True if the interface exists and is unnumbered"""
return ifname in get_unnumbered_interfaces(yaml)
def get_lcp(yaml, ifname): def get_lcp(yaml, ifname):
"""Returns the LCP of the interface. If the interface is a sub-interface with L3 """Returns the LCP of the interface. If the interface is a sub-interface with L3
enabled, synthesize it based on its parent, using smart QinQ syntax. enabled, synthesize it based on its parent, using smart QinQ syntax.
@ -494,6 +516,37 @@ def validate_interfaces(yaml):
) )
result = False result = False
if "unnumbered" in iface:
target = iface["unnumbered"]
_, target_iface = loopback.get_by_name(yaml, target)
if not target_iface:
_, target_iface = get_by_name(yaml, target)
if not target_iface:
msgs.append(
f"interface {ifname} unnumbered target {target} does not exist"
)
result = False
if is_l2(yaml, target):
msgs.append(
f"interface {ifname} unnumbered target {target} cannot be in L2 mode"
)
result = False
if is_unnumbered(yaml, target):
msgs.append(
f"interface {ifname} unnumbered target {target} cannot also be unnumbered"
)
result = False
if ifname == target:
msgs.append(
f"interface {ifname} unnumbered target cannot point to itself"
)
result = False
if has_address(yaml, ifname):
msgs.append(
f"interface {ifname} cannot also have addresses when it is unnumbered"
)
result = False
if "addresses" in iface: if "addresses" in iface:
for addr in iface["addresses"]: for addr in iface["addresses"]:
if not address.is_allowed(yaml, ifname, iface["addresses"], addr): if not address.is_allowed(yaml, ifname, iface["addresses"], addr):
@ -625,6 +678,37 @@ def validate_interfaces(yaml):
) )
result = False result = False
if "unnumbered" in sub_iface:
target = sub_iface["unnumbered"]
_, target_iface = loopback.get_by_name(yaml, target)
if not target_iface:
_, target_iface = get_by_name(yaml, target)
if not target_iface:
msgs.append(
f"sub-interface {sub_ifname} unnumbered target {target} does not exist"
)
result = False
if is_l2(yaml, target):
msgs.append(
f"sub-interface {sub_ifname} unnumbered target {target} cannot be in L2 mode"
)
result = False
if is_unnumbered(yaml, target):
msgs.append(
f"sub-interface {sub_ifname} unnumbered target {target} cannot also be unnumbered"
)
result = False
if sub_ifname == target:
msgs.append(
f"sub-interface {sub_ifname} unnumbered target cannot point to itself"
)
result = False
if has_address(yaml, sub_ifname):
msgs.append(
f"sub-interface {sub_ifname} cannot also have addresses when it is unnumbered"
)
result = False
if has_address(yaml, sub_ifname): if has_address(yaml, sub_ifname):
if not encap or not encap["exact-match"]: if not encap or not encap["exact-match"]:
msgs.append( msgs.append(

View File

@ -26,12 +26,12 @@ class TestInterfaceMethods(unittest.TestCase):
def test_enumerators(self): def test_enumerators(self):
ifs = interface.get_interfaces(self.cfg) ifs = interface.get_interfaces(self.cfg)
self.assertEqual(len(ifs), 19) self.assertEqual(len(ifs), 27)
self.assertIn("GigabitEthernet1/0/1", ifs) self.assertIn("GigabitEthernet1/0/1", ifs)
self.assertIn("GigabitEthernet1/0/1.200", ifs) self.assertIn("GigabitEthernet1/0/1.200", ifs)
ifs = interface.get_sub_interfaces(self.cfg) ifs = interface.get_sub_interfaces(self.cfg)
self.assertEqual(len(ifs), 13) self.assertEqual(len(ifs), 17)
self.assertNotIn("GigabitEthernet1/0/1", ifs) self.assertNotIn("GigabitEthernet1/0/1", ifs)
self.assertIn("GigabitEthernet1/0/1.200", ifs) self.assertIn("GigabitEthernet1/0/1.200", ifs)
self.assertIn("GigabitEthernet1/0/1.201", ifs) self.assertIn("GigabitEthernet1/0/1.201", ifs)
@ -261,7 +261,7 @@ class TestInterfaceMethods(unittest.TestCase):
def test_get_phys(self): def test_get_phys(self):
phys = interface.get_phys(self.cfg) phys = interface.get_phys(self.cfg)
self.assertEqual(len(phys), 6) self.assertEqual(len(phys), 10)
self.assertIn("GigabitEthernet1/0/0", phys) self.assertIn("GigabitEthernet1/0/0", phys)
self.assertNotIn("GigabitEthernet1/0/0.100", phys) self.assertNotIn("GigabitEthernet1/0/0.100", phys)
@ -284,3 +284,20 @@ class TestInterfaceMethods(unittest.TestCase):
self.assertFalse(interface.is_mpls(self.cfg, "GigabitEthernet1/0/0")) self.assertFalse(interface.is_mpls(self.cfg, "GigabitEthernet1/0/0"))
self.assertFalse(interface.is_mpls(self.cfg, "GigabitEthernet1/0/0.100")) self.assertFalse(interface.is_mpls(self.cfg, "GigabitEthernet1/0/0.100"))
self.assertFalse(interface.is_mpls(self.cfg, "notexist")) self.assertFalse(interface.is_mpls(self.cfg, "notexist"))
def test_get_unnumbered_interfaces(self):
unnumbered_ifaces = interface.get_unnumbered_interfaces(self.cfg)
self.assertEqual(len(unnumbered_ifaces), 5)
self.assertNotIn("GigabitEthernet4/0/0", unnumbered_ifaces)
self.assertIn("GigabitEthernet4/0/1", unnumbered_ifaces)
self.assertNotIn("GigabitEthernet4/0/3.100", unnumbered_ifaces)
self.assertIn("GigabitEthernet4/0/3.101", unnumbered_ifaces)
def test_is_unnumbered(self):
self.assertFalse(interface.is_unnumbered(self.cfg, "GigabitEthernet4/0/0"))
self.assertTrue(interface.is_unnumbered(self.cfg, "GigabitEthernet4/0/1"))
self.assertTrue(interface.is_unnumbered(self.cfg, "GigabitEthernet4/0/2"))
self.assertFalse(interface.is_unnumbered(self.cfg, "GigabitEthernet4/0/3.100"))
self.assertTrue(interface.is_unnumbered(self.cfg, "GigabitEthernet4/0/3.101"))
self.assertTrue(interface.is_unnumbered(self.cfg, "GigabitEthernet4/0/3.102"))
self.assertTrue(interface.is_unnumbered(self.cfg, "GigabitEthernet4/0/3.103"))

View File

@ -50,6 +50,7 @@ interface:
lcp: str(max=15,matches='[a-z]+[a-z0-9-]*',required=False) lcp: str(max=15,matches='[a-z]+[a-z0-9-]*',required=False)
mtu: int(min=128,max=9216,required=False) mtu: int(min=128,max=9216,required=False)
addresses: list(ip_interface(),min=1,max=6,required=False) addresses: list(ip_interface(),min=1,max=6,required=False)
unnumbered: str(required=False)
sub-interfaces: map(include('sub-interface'),key=int(min=1,max=4294967295),required=False) sub-interfaces: map(include('sub-interface'),key=int(min=1,max=4294967295),required=False)
l2xc: str(required=False) l2xc: str(required=False)
state: enum('up', 'down', required=False) state: enum('up', 'down', required=False)
@ -61,6 +62,7 @@ sub-interface:
lcp: str(max=15,matches='[a-z]+[a-z0-9-]*',required=False) lcp: str(max=15,matches='[a-z]+[a-z0-9-]*',required=False)
mtu: int(min=128,max=9216,required=False) mtu: int(min=128,max=9216,required=False)
addresses: list(ip_interface(),required=False) addresses: list(ip_interface(),required=False)
unnumbered: str(required=False)
encapsulation: include('encapsulation',required=False) encapsulation: include('encapsulation',required=False)
l2xc: str(required=False) l2xc: str(required=False)
state: enum('up', 'down', required=False) state: enum('up', 'down', required=False)

View File

@ -1,3 +1,7 @@
loopbacks:
loop0:
addresses: [ 192.0.2.1/32, 2001:db8::1/128 ]
interfaces: interfaces:
GigabitEthernet1/0/0: GigabitEthernet1/0/0:
sub-interfaces: sub-interfaces:
@ -75,3 +79,20 @@ interfaces:
l2xc: GigabitEthernet3/0/2.200 l2xc: GigabitEthernet3/0/2.200
200: 200:
description: "This interface does not connect back to Gi3/0/2.100. Strange, but valid." description: "This interface does not connect back to Gi3/0/2.100. Strange, but valid."
GigabitEthernet4/0/0:
addresses: [ 192.0.2.129/30, 2001:db8:10::1/64 ]
GigabitEthernet4/0/1:
unnumbered: GigabitEthernet4/0/0
GigabitEthernet4/0/2:
unnumbered: loop0
GigabitEthernet4/0/3:
sub-interfaces:
100:
addresses: [ 192.168.2.133/30, 2001:db8:11::1/64 ]
101:
unnumbered: loop0
102:
unnumbered: GigabitEthernet4/0/1
103:
unnumbered: GigabitEthernet4/0/3.100

View File

@ -0,0 +1,43 @@
test:
description: "Test that is meant to pass"
errors:
count: 0
---
loopbacks:
loop0:
mtu: 9216
addresses: [ 192.0.2.1/32, 2001:db8:1::1/128 ]
interfaces:
GigabitEthernet1/0/0:
addresses: [ 94.142.244.85/24, 2a02:898::146:1/64 ]
sub-interfaces:
100:
addresses: [ 94.142.241.185/29, 2a02:898:146::1/64 ]
101:
unnumbered: loop0
GigabitEthernet2/0/0:
addresses: [ 192.0.2.5/30, 2001:db8:2::1/64 ]
GigabitEthernet2/0/1:
unnumbered: GigabitEthernet2/0/0
GigabitEthernet3/0/0:
sub-interfaces:
100:
addresses: [ 192.0.2.9/30, 2001:db8:3::1/64 ]
101:
unnumbered: loop0
GigabitEthernet3/0/1:
unnumbered: GigabitEthernet3/0/0.100
GigabitEthernet4/0/0:
addresses: [ 192.0.2.13/30, 2001:db8:4::1/64 ]
GigabitEthernet4/0/1:
sub-interfaces:
100:
unnumbered: GigabitEthernet4/0/0
101:
unnumbered: loop0

View File

@ -0,0 +1,37 @@
test:
description: "Nonexistent unnumbered target"
errors:
expected:
- "^sub-interface .* unnumbered target .* does not exist"
- "^interface .* unnumbered target .* does not exist"
count: 4
---
loopbacks:
loop0:
mtu: 9216
addresses: [ 192.0.2.1/32, 2001:db8:1::1/128 ]
interfaces:
GigabitEthernet1/0/0:
sub-interfaces:
101:
description: "Error: non existent loopback device"
unnumbered: loop1
GigabitEthernet2/0/0:
addresses: [ 192.0.2.5/30, 2001:db8:2::1/64 ]
GigabitEthernet2/0/1:
description: "Error: non existent phy"
unnumbered: GigabitEthernet2/0/2
GigabitEthernet3/0/0:
sub-interfaces:
100:
addresses: [ 192.0.2.9/30, 2001:db8:3::1/64 ]
101:
description: "Error: non existent loopback device"
unnumbered: loop2
GigabitEthernet3/0/1:
description: "Error: non existent sub-int"
unnumbered: GigabitEthernet3/0/1.100

View File

@ -0,0 +1,32 @@
test:
description: "Unnumbered cannot point to self; Unnumbered cannot be a target of another unnumbered"
errors:
expected:
- "(sub-)?interface .* unnumbered target cannot point to itself"
- "(sub-)?interface .* unnumbered target .* cannot also be unnumbered"
count: 8
---
interfaces:
GigabitEthernet1/0/0:
description: "Cannot point to the same interface"
unnumbered: GigabitEthernet1/0/0
sub-interfaces:
100:
description: "Cannot point to the same sub-interface"
unnumbered: GigabitEthernet1/0/0.100
GigabitEthernet2/0/0:
description: "Cannot point to Gi2/0/1, as that interface is itself unnumbered"
unnumbered: GigabitEthernet2/0/1
GigabitEthernet2/0/1:
description: "Cannot point to Gi2/0/0, as that interface is itself unnumbered"
unnumbered: GigabitEthernet2/0/0
GigabitEthernet3/0/0:
sub-interfaces:
100:
description: "Cannot point to Gi3/0/0.101, as that interface is itself unnumbered"
unnumbered: GigabitEthernet3/0/0.101
101:
description: "Cannot point to Gi3/0/0.100, as that interface is itself unnumbered"
unnumbered: GigabitEthernet3/0/0.100

View File

@ -0,0 +1,32 @@
test:
description: "Unnumbered targets cannot have addresses"
errors:
expected:
- "(sub-)?interface .* cannot also have addresses when it is unnumbered"
count: 3
---
loopbacks:
loop0:
mtu: 9216
addresses: [ 192.0.2.1/32, 2001:db8:1::1/128 ]
interfaces:
GigabitEthernet1/0/0:
sub-interfaces:
101:
addresses: [ 192.0.2.129/30, 2001:db8:10::1/128 ]
unnumbered: loop0
GigabitEthernet2/0/0:
addresses: [ 192.0.2.5/30, 2001:db8:2::1/64 ]
GigabitEthernet2/0/1:
addresses: [ 192.0.2.133/30, 2001:db8:11::1/128 ]
unnumbered: GigabitEthernet2/0/0
GigabitEthernet3/0/0:
sub-interfaces:
100:
addresses: [ 192.0.2.9/30, 2001:db8:3::1/64 ]
101:
addresses: [ 192.0.2.137/30, 2001:db8:12::1/128 ]
unnumbered: GigabitEthernet3/0/0.100

View File

@ -0,0 +1,91 @@
test:
description: "Unnumbered targets cannot be in l2 mode"
errors:
expected:
- "(sub-)?interface .* unnumbered target .* cannot be in L2 mode"
count: 2
---
loopbacks:
loop0:
lcp: "loop0"
mtu: 3000
addresses: [ 192.0.2.1/30, 2001:db8::1/64 ]
bondethernets:
BondEthernet0:
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
interfaces:
GigabitEthernet1/0/0:
mtu: 3000
GigabitEthernet1/0/1:
mtu: 3000
GigabitEthernet2/0/0:
mtu: 9000
sub-interfaces:
100:
mtu: 2000
GigabitEthernet2/0/1:
mtu: 9000
sub-interfaces:
100:
mtu: 2000
GigabitEthernet3/0/0:
mtu: 3000
GigabitEthernet3/0/1:
mtu: 3000
GigabitEthernet3/0/2:
mtu: 5000
l2xc: GigabitEthernet3/0/3
GigabitEthernet3/0/3:
mtu: 5000
l2xc: GigabitEthernet3/0/2
GigabitEthernet4/0/0:
mtu: 3000
description: "Cannot be unnumbered off of a bond-member"
unnumbered: GigabitEthernet3/0/0
GigabitEthernet4/0/0:
mtu: 2000
description: "Cannot be unnumbered off of an l2xc"
unnumbered: GigabitEthernet3/0/2
GigabitEthernet4/0/2:
mtu: 3000
description: "Cannot be unnumbered off of a bridge-domain member"
unnumbered: BondEthernet0.100
sub-interfaces:
10:
description: "Cannot be unnumbered off of a bond member"
unnumbered: GigabitEthernet3/0/1
100:
description: "Cannot be unnumbered off of a bridge-domain member"
unnumbered: BondEthernet0.100
200:
description: "Cannot be unnumbered off of an l2xc"
unnumbered: BondEthernet0.200
BondEthernet0:
mtu: 3000
sub-interfaces:
100:
mtu: 2000
200:
mtu: 2000
l2xc: BondEthernet0.201
201:
mtu: 2000
l2xc: BondEthernet0.200
bridgedomains:
bd10:
description: "Bridge Domain 10"
mtu: 3000
bvi: loop0
interfaces: [ GigabitEthernet1/0/0, GigabitEthernet1/0/1, BondEthernet0 ]
bd11:
description: "Bridge Domain 11"
mtu: 2000
interfaces: [ GigabitEthernet2/0/0.100, GigabitEthernet2/0/1.100 ]