Merge pull request #13 from pimvanpelt/mpls_iface

Support MPLS Interfaces
This commit is contained in:
Pim van Pelt
2023-06-23 00:17:57 +02:00
committed by GitHub
12 changed files with 106 additions and 9 deletions

View File

@ -73,6 +73,9 @@ following fields:
in CIDR format. VPP requires IP addresses to be unique in the entire dataplane, with one in CIDR format. VPP requires IP addresses to be unique in the entire dataplane, with one
notable exception: Multiple IP addresses in the same prefix/len can be added on one and the notable exception: Multiple IP addresses in the same prefix/len can be added on one and the
same interface. same interface.
* ***mpls***: An optional boolean that configures MPLS on the interface or sub-interface. The
default value is `false`, if the field is not specified, which means MPLS will not be enabled.
This allows BVIs, represented by Loopbacks, to participate in MPLS .
Although VPP would allow it, `vppcfg` does not allow for loopbacks to have sub-interfaces. Although VPP would allow it, `vppcfg` does not allow for loopbacks to have sub-interfaces.
@ -87,6 +90,7 @@ loopbacks:
lcp: bvi1 lcp: bvi1
mtu: 9000 mtu: 9000
addresses: [ 10.0.1.1/24, 10.0.1.2/24, 2001:db8:1::1/64 ] addresses: [ 10.0.1.1/24, 10.0.1.2/24, 2001:db8:1::1/64 ]
mpls: true
``` ```
### Bridge Domains ### Bridge Domains
@ -306,6 +310,8 @@ exist as a PHY in VPP (ie. `HundredGigabitEthernet12/0/0`) or as a specified `Bo
If it is not specified, the link is considered admin 'up'. If it is not specified, the link is considered admin 'up'.
* ***device-type***: An optional interface type in VPP. Currently the only supported vlaue is * ***device-type***: An optional interface type in VPP. Currently the only supported vlaue is
`dpdk`, and it is used to generate correct mock interfaces if the `--novpp` flag is used. `dpdk`, and it is used to generate correct mock interfaces if the `--novpp` flag is used.
* ***mpls***: An optional boolean that configures MPLS on the interface or sub-interface. The
default value is `false`, if the field is not specified, which means MPLS will not be enabled.
Further, top-level interfaces, that is to say those that do not have an encapsulation, are permitted Further, top-level interfaces, that is to say those that do not have an encapsulation, are permitted
to have any number of sub-interfaces specified by `subid`, an integer between [0,2G), which further to have any number of sub-interfaces specified by `subid`, an integer between [0,2G), which further
@ -330,6 +336,7 @@ interfaces:
lcp: "ice0" lcp: "ice0"
mtu: 9000 mtu: 9000
addresses: [ 192.0.2.1/30, 2001:db8:1::1/64 ] addresses: [ 192.0.2.1/30, 2001:db8:1::1/64 ]
mpls: true
sub-interfaces: sub-interfaces:
1234: 1234:
mtu: 9000 mtu: 9000
@ -338,6 +345,7 @@ interfaces:
1235: 1235:
mtu: 1500 mtu: 1500
lcp: "ice0.qinq" lcp: "ice0.qinq"
mpls: true
addresses: [ 192.0.2.9/30, 2001:db8:3::1/64 ] addresses: [ 192.0.2.9/30, 2001:db8:3::1/64 ]
encapsulation: encapsulation:
dot1q: 1234 dot1q: 1234

View File

@ -701,3 +701,14 @@ def validate_interfaces(yaml):
result = False result = False
return result, msgs return result, msgs
def is_mpls(yaml, ifname):
"""Returns True if the interface exists and has mpls enabled. Returns false otherwise."""
ifname, iface = get_by_name(yaml, ifname)
try:
if iface["mpls"] == True:
return True
except:
pass
return False

View File

@ -96,3 +96,14 @@ def validate_loopbacks(yaml):
result = False result = False
return result, msgs return result, msgs
def is_mpls(yaml, ifname):
"""Returns True if the loopback exists and has mpls enabled. Returns false otherwise."""
ifname, iface = get_by_name(yaml, ifname)
try:
if iface["mpls"] == True:
return True
except:
pass
return False

View File

@ -277,3 +277,10 @@ class TestInterfaceMethods(unittest.TestCase):
self.assertFalse( self.assertFalse(
interface.get_admin_state(self.cfg, "GigabitEthernet1/0/0.102") interface.get_admin_state(self.cfg, "GigabitEthernet1/0/0.102")
) )
def test_is_mpls(self):
self.assertTrue(interface.is_mpls(self.cfg, "GigabitEthernet1/0/1"))
self.assertTrue(interface.is_mpls(self.cfg, "GigabitEthernet1/0/1.101"))
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, "notexist"))

View File

@ -50,3 +50,8 @@ class TestLoopbackMethods(unittest.TestCase):
self.assertIn("loop1", ifs) self.assertIn("loop1", ifs)
self.assertIn("loop2", ifs) self.assertIn("loop2", ifs)
self.assertNotIn("loop-noexist", ifs) self.assertNotIn("loop-noexist", ifs)
def test_is_mpls(self):
self.assertTrue(loopback.is_mpls(self.cfg, "loop1"))
self.assertFalse(loopback.is_mpls(self.cfg, "loop2"))
self.assertFalse(loopback.is_mpls(self.cfg, "loop-noexist"))

View File

@ -20,7 +20,8 @@ interfaces:
lcp: "ice12-0-0" lcp: "ice12-0-0"
mac: f2:01:00:12:00:00 mac: f2:01:00:12:00:00
mtu: 9000 mtu: 9000
addresses: [ 192.0.2.17/30, 2001:DB8:3::1/64 ] addresses: [ 192.0.2.17/30, 2001:db8:3::1/64 ]
mpls: true
sub-interfaces: sub-interfaces:
1234: 1234:
mtu: 1200 mtu: 1200
@ -90,6 +91,7 @@ loopbacks:
mtu: 1500 mtu: 1500
mac: 02:de:ad:11:be:ef mac: 02:de:ad:11:be:ef
addresses: [ 10.0.2.1/24, 2001:db8:2::1/64 ] addresses: [ 10.0.2.1/24, 2001:db8:2::1/64 ]
mpls: true
bridgedomains: bridgedomains:
bd1: bd1:

View File

@ -33,6 +33,7 @@ loopback:
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)
mpls: bool(required=False)
--- ---
bondethernet: bondethernet:
description: str(exclude='\'"',len=64,required=False) description: str(exclude='\'"',len=64,required=False)
@ -50,6 +51,7 @@ interface:
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)
mpls: bool(required=False)
device-type: enum('dpdk', required=False) device-type: enum('dpdk', required=False)
--- ---
sub-interface: sub-interface:
@ -60,6 +62,7 @@ sub-interface:
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)
mpls: bool(required=False)
--- ---
encapsulation: encapsulation:
dot1q: int(min=1,max=4095,required=False) dot1q: int(min=1,max=4095,required=False)

View File

@ -23,6 +23,7 @@ interfaces:
mtu: 9216 mtu: 9216
lcp: "e1" lcp: "e1"
addresses: [ "192.0.2.1/30", "2001:db8:1::1/64" ] addresses: [ "192.0.2.1/30", "2001:db8:1::1/64" ]
mpls: true
sub-interfaces: sub-interfaces:
100: 100:
lcp: "foo" lcp: "foo"
@ -33,6 +34,7 @@ interfaces:
exact-match: True exact-match: True
lcp: "e1.100" lcp: "e1.100"
addresses: [ "10.0.2.1/30" ] addresses: [ "10.0.2.1/30" ]
mpls: true
102: 102:
encapsulation: encapsulation:
dot1ad: 100 dot1ad: 100

View File

@ -6,6 +6,7 @@ loopbacks:
mtu: 2000 mtu: 2000
lcp: "loop56789012345" lcp: "loop56789012345"
addresses: [ 192.0.2.1/29, 2001:db8::1/64 ] addresses: [ 192.0.2.1/29, 2001:db8::1/64 ]
mpls: true
loop2: loop2:
description: "Loopback, invalid because it has an address but no LCP" description: "Loopback, invalid because it has an address but no LCP"
mtu: 2000 mtu: 2000

View File

@ -108,6 +108,8 @@ class Dumper(VPPApi):
loop["addresses"] = self.cache["interface_addresses"][ loop["addresses"] = self.cache["interface_addresses"][
iface.sw_if_index iface.sw_if_index
] ]
if iface.sw_if_index in self.cache["interface_mpls"]:
loop["mpls"] = self.cache["interface_mpls"][iface.sw_if_index]
config["loopbacks"][iface.interface_name] = loop config["loopbacks"][iface.interface_name] = loop
elif iface.interface_dev_type in [ elif iface.interface_dev_type in [
"bond", "bond",

View File

@ -947,6 +947,9 @@ class Reconciler:
if not self.__sync_phys(): if not self.__sync_phys():
self.logger.warning("Could not sync PHYs in VPP") self.logger.warning("Could not sync PHYs in VPP")
ret = False ret = False
if not self.__sync_mpls_state():
self.logger.warning("Could not sync interface MPLS state in VPP")
ret = False
if not self.__sync_admin_state(): if not self.__sync_admin_state():
self.logger.warning("Could not sync interface adminstate in VPP") self.logger.warning("Could not sync interface adminstate in VPP")
ret = False ret = False
@ -1291,6 +1294,36 @@ class Reconciler:
ret = False ret = False
return ret return ret
def __sync_mpls_state(self):
"""Synchronize the VPP Dataplane configuration for interface and loopback MPLS state"""
for ifname in interface.get_interfaces(self.cfg) + loopback.get_loopbacks(
self.cfg
):
if ifname.startswith("loop"):
vpp_ifname, config_iface = loopback.get_by_name(self.cfg, ifname)
else:
vpp_ifname, config_iface = interface.get_by_name(self.cfg, ifname)
try:
config_mpls = config_iface["mpls"]
except KeyError:
config_mpls = False
vpp_mpls = False
if vpp_ifname in self.vpp.cache["interface_names"]:
sw_if_index = self.vpp.cache["interface_names"][vpp_ifname]
try:
vpp_mpls = self.vpp.cache["interface_mpls"][sw_if_index]
except KeyError:
pass
if vpp_mpls != config_mpls:
state = "disable"
if config_mpls:
state = "enable"
cli = f"set interface mpls {vpp_ifname} {state}"
self.cli["sync"].append(cli)
return True
def __sync_addresses(self): def __sync_addresses(self):
"""Synchronize the VPP Dataplane configuration for interface addresses""" """Synchronize the VPP Dataplane configuration for interface addresses"""
for ifname in interface.get_interfaces(self.cfg) + loopback.get_loopbacks( for ifname in interface.get_interfaces(self.cfg) + loopback.get_loopbacks(

View File

@ -119,6 +119,7 @@ class VPPApi:
"interface_names": {}, "interface_names": {},
"interfaces": {}, "interfaces": {},
"interface_addresses": {}, "interface_addresses": {},
"interface_mpls": {},
"bondethernets": {}, "bondethernets": {},
"bondethernet_members": {}, "bondethernet_members": {},
"bridgedomains": {}, "bridgedomains": {},
@ -215,7 +216,7 @@ class VPPApi:
enumerating the 'interfaces' scope from yaml_config""" enumerating the 'interfaces' scope from yaml_config"""
if not "interfaces" in yaml_config: if not "interfaces" in yaml_config:
self.logger.error(f"YAML config does not contain any interfaces") self.logger.error("YAML config does not contain any interfaces")
return False return False
self.logger.debug(f"config: {yaml_config['interfaces']}") self.logger.debug(f"config: {yaml_config['interfaces']}")
@ -305,9 +306,7 @@ class VPPApi:
self.cache["lcps"][lcp.phy_sw_if_index] = lcp self.cache["lcps"][lcp.phy_sw_if_index] = lcp
self.lcp_enabled = True self.lcp_enabled = True
except AttributeError as err: except AttributeError as err:
self.logger.warning( self.logger.warning(f"LinuxCP API not found - missing plugin: {err}")
f"linux-cp not found, will not reconcile Linux Control Plane: {err}"
)
self.logger.debug("Retrieving interfaces") self.logger.debug("Retrieving interfaces")
api_response = self.vpp.api.sw_interface_dump() api_response = self.vpp.api.sw_interface_dump()
@ -332,6 +331,16 @@ class VPPApi:
str(addr.prefix) str(addr.prefix)
) )
try: ## TODO(pim): Remove after 23.10 release
self.logger.debug("Retrieving interface MPLS state")
api_response = self.vpp.api.mpls_interface_dump()
for iface in api_response:
self.cache["interface_mpls"][iface.sw_if_index] = True
except AttributeError:
self.logger.warning(
f"MPLS state retrieval requires https://gerrit.fd.io/r/c/vpp/+/39022"
)
self.logger.debug("Retrieving bondethernets") self.logger.debug("Retrieving bondethernets")
api_response = self.vpp.api.sw_bond_interface_dump() api_response = self.vpp.api.sw_bond_interface_dump()
for iface in api_response: for iface in api_response:
@ -349,10 +358,13 @@ class VPPApi:
for bridge in api_response: for bridge in api_response:
self.cache["bridgedomains"][bridge.bd_id] = bridge self.cache["bridgedomains"][bridge.bd_id] = bridge
try:
self.logger.debug("Retrieving vxlan_tunnels") self.logger.debug("Retrieving vxlan_tunnels")
api_response = self.vpp.api.vxlan_tunnel_v2_dump() api_response = self.vpp.api.vxlan_tunnel_v2_dump()
for vxlan in api_response: for vxlan in api_response:
self.cache["vxlan_tunnels"][vxlan.sw_if_index] = vxlan self.cache["vxlan_tunnels"][vxlan.sw_if_index] = vxlan
except AttributeError as err:
self.logger.warning(f"VXLAN API not found - missing plugin: {err}")
self.logger.debug("Retrieving L2 Cross Connects") self.logger.debug("Retrieving L2 Cross Connects")
api_response = self.vpp.api.l2_xconnect_dump() api_response = self.vpp.api.l2_xconnect_dump()