Add TAP support

- based on previously submitted schema and validation, can add a TAP
  with host netns, bridge and MTU.
- detect diffs in __tap_has_diff(), used to prune TAPs that must change
- add prune_taps() and create_taps() in the reconciler
- add Dumper() logic to emit YAML config for TAPs
- Move tap_is_lcp() into the VPPApi() class, so it can be reused

Add lots of test cases in intest/*.yaml and example.yaml - full
regression and integration and unit/YAML tests pass on this change.
This commit is contained in:
Pim van Pelt
2022-04-12 08:57:05 +00:00
parent 95f56bc865
commit 6637820021
6 changed files with 213 additions and 26 deletions

View File

@ -11,7 +11,8 @@ interfaces:
description: "LAG #2" description: "LAG #2"
HundredGigabitEthernet12/0/0: HundredGigabitEthernet12/0/0:
description: Not Used mtu: 1500
description: "bridged with tap"
HundredGigabitEthernet12/0/1: HundredGigabitEthernet12/0/1:
description: Not Used description: Not Used
@ -42,3 +43,23 @@ interfaces:
encapsulation: encapsulation:
dot1ad: 501 dot1ad: 501
exact-match: False exact-match: False
tap100:
mtu: 1500
loopbacks:
loop100:
lcp: "bvi100"
addresses: [ 10.1.2.1/24 ]
bridgedomains:
bd100:
description: "Bridge Domain 100"
mtu: 1500
bvi: loop100
interfaces: [ HundredGigabitEthernet12/0/0, tap100 ]
taps:
tap100:
host:
name: vpp-tap100
mtu: 1500

View File

@ -66,6 +66,8 @@ interfaces:
exact-match: False exact-match: False
vxlan_tunnel1: vxlan_tunnel1:
mtu: 2000 mtu: 2000
tap100:
mtu: 9000
loopbacks: loopbacks:
loop0: loop0:
@ -90,3 +92,9 @@ vxlan_tunnels:
local: 192.0.2.1 local: 192.0.2.1
remote: 192.0.2.2 remote: 192.0.2.2
vni: 101 vni: 101
taps:
tap100:
host:
name: vpp-tap
mtu: 9000

34
intest/hippo14.yaml Normal file
View File

@ -0,0 +1,34 @@
interfaces:
GigabitEthernet3/0/0:
mtu: 9000
state: up
sub-interfaces:
100:
mtu: 9000
l2xc: tap100
GigabitEthernet3/0/1:
mtu: 1500
mac: 00:25:90:0c:05:01
state: down
description: Not Used
HundredGigabitEthernet12/0/0:
mtu: 1500
mac: b4:96:91:b3:b1:10
state: down
description: Not Used
HundredGigabitEthernet12/0/1:
mtu: 1500
mac: b4:96:91:b3:b1:11
state: down
description: Not Used
tap100:
mtu: 9000
l2xc: GigabitEthernet3/0/0.100
taps:
tap100:
host:
name: vpp-tap100
mac: 02:01:be:ef:ca:fe
mtu: 9000

View File

@ -28,7 +28,7 @@ class Dumper(VPPApi):
self.logger.info("Wrote YAML config to %s" % (outfile)) self.logger.info("Wrote YAML config to %s" % (outfile))
def cache_to_config(self): def cache_to_config(self):
config = {"loopbacks": {}, "bondethernets": {}, "interfaces": {}, "bridgedomains": {}, "vxlan_tunnels": {} } config = {"loopbacks": {}, "bondethernets": {}, "interfaces": {}, "bridgedomains": {}, "vxlan_tunnels": {}, "taps": {} }
for idx, bond_iface in self.cache['bondethernets'].items(): for idx, bond_iface in self.cache['bondethernets'].items():
bond = {"description": ""} bond = {"description": ""}
if bond_iface.sw_if_index in self.cache['bondethernet_members']: if bond_iface.sw_if_index in self.cache['bondethernet_members']:
@ -62,7 +62,7 @@ class Dumper(VPPApi):
if len(self.cache['interface_addresses'][iface.sw_if_index]) > 0: if len(self.cache['interface_addresses'][iface.sw_if_index]) > 0:
loop['addresses'] = self.cache['interface_addresses'][iface.sw_if_index] loop['addresses'] = self.cache['interface_addresses'][iface.sw_if_index]
config['loopbacks'][iface.interface_name] = loop config['loopbacks'][iface.interface_name] = loop
elif iface.interface_dev_type in ['bond', 'VXLAN', 'dpdk']: elif iface.interface_dev_type in ['bond', 'VXLAN', 'dpdk', 'virtio']:
i = {"description": "" } i = {"description": "" }
if iface.sw_if_index in self.cache['lcps']: if iface.sw_if_index in self.cache['lcps']:
i['lcp'] = self.cache['lcps'][iface.sw_if_index].host_if_name i['lcp'] = self.cache['lcps'][iface.sw_if_index].host_if_name
@ -77,6 +77,10 @@ class Dumper(VPPApi):
if iface.interface_dev_type == 'dpdk': if iface.interface_dev_type == 'dpdk':
i['mac'] = str(iface.l2_address) i['mac'] = str(iface.l2_address)
if self.tap_is_lcp(iface.interface_name):
continue
i['mtu'] = iface.mtu[0] i['mtu'] = iface.mtu[0]
if iface.sub_number_of_tags == 0: if iface.sub_number_of_tags == 0:
config['interfaces'][iface.interface_name] = i config['interfaces'][iface.interface_name] = i
@ -109,6 +113,25 @@ class Dumper(VPPApi):
"remote": str(iface.dst_address) } "remote": str(iface.dst_address) }
config['vxlan_tunnels'][vpp_iface.interface_name] = vxlan config['vxlan_tunnels'][vpp_iface.interface_name] = vxlan
for idx, iface in self.cache['taps'].items():
vpp_tap = self.cache['taps'][iface.sw_if_index]
vpp_iface = self.cache['interfaces'][vpp_tap.sw_if_index]
tap = { "description": "",
"tx-ring-size": vpp_tap.tx_ring_sz,
"rx-ring-size": vpp_tap.rx_ring_sz,
"host": {
"mac": str(vpp_tap.host_mac_addr),
"name": vpp_tap.host_if_name,
} }
if vpp_tap.host_mtu_size > 0:
tap['host']['mtu'] = vpp_tap.host_mtu_size
if vpp_tap.host_namespace:
tap['host']['namespace'] = vpp_tap.host_namespace
if vpp_tap.host_bridge:
tap['host']['bridge'] = vpp_tap.host_bridge
config['taps'][vpp_iface.interface_name] = tap
for idx, iface in self.cache['bridgedomains'].items(): for idx, iface in self.cache['bridgedomains'].items():
# self.logger.info("%d: %s" % (idx, iface)) # self.logger.info("%d: %s" % (idx, iface))
bridge_name = "bd%d" % idx bridge_name = "bd%d" % idx

View File

@ -21,6 +21,7 @@ import config.bondethernet as bondethernet
import config.bridgedomain as bridgedomain import config.bridgedomain as bridgedomain
import config.vxlan_tunnel as vxlan_tunnel import config.vxlan_tunnel as vxlan_tunnel
import config.lcp as lcp import config.lcp as lcp
import config.tap as tap
from vpp.vppapi import VPPApi from vpp.vppapi import VPPApi
class Reconciler(): class Reconciler():
@ -95,6 +96,9 @@ class Reconciler():
if not self.prune_sub_interfaces(): if not self.prune_sub_interfaces():
self.logger.warning("Could not prune Sub Interfaces from VPP") self.logger.warning("Could not prune Sub Interfaces from VPP")
ret = False ret = False
if not self.prune_taps():
self.logger.warning("Could not prune TAPs from VPP")
ret = False
if not self.prune_vxlan_tunnels(): if not self.prune_vxlan_tunnels():
self.logger.warning("Could not prune VXLAN Tunnels from VPP") self.logger.warning("Could not prune VXLAN Tunnels from VPP")
ret = False ret = False
@ -236,6 +240,41 @@ class Reconciler():
self.vpp.cache_remove_l2xc(l2xc) self.vpp.cache_remove_l2xc(l2xc)
return True return True
def __tap_has_diff(self, ifname):
""" Returns True if the given ifname (tap0) has different attributes between VPP
and the given configuration, or if either does not exist.
Returns False if the TAP is a Linux Control Plane LIP.
Returns False if they are identical."""
if not ifname in self.vpp.cache['interface_names']:
return True
vpp_iface = self.vpp.cache['interface_names'][ifname]
vpp_tap = self.vpp.cache['taps'][vpp_iface.sw_if_index]
config_ifname, config_iface = tap.get_by_name(self.cfg, ifname)
if not config_iface:
return True
if self.vpp.tap_is_lcp(ifname):
return False
if 'name' in config_iface['host'] and config_iface['host']['name'] != vpp_tap.host_if_name:
self.logger.info("TAP diff: hostname")
return True
if 'mtu' in config_iface['host'] and config_iface['host']['mtu'] != vpp_tap.host_mtu_size:
return True
if 'mac' in config_iface['host'] and config_iface['host']['mac'] != str(vpp_tap.host_mac_addr):
self.logger.info("TAP diff: mac")
return True
if 'bridge' in config_iface['host'] and config_iface['host']['bridge'] != vpp_tap.host_bridge:
self.logger.info("TAP diff: bridge")
return True
if 'namespace' in config_iface['host'] and config_iface['host']['namespace'] != vpp_tap.host_namespace:
self.logger.info("TAP diff: namespace")
return True
return False
def __bond_has_diff(self, ifname): def __bond_has_diff(self, ifname):
""" Returns True if the given ifname (BondEthernet0) have different attributes, """ Returns True if the given ifname (BondEthernet0) have different attributes,
or if either does not exist. or if either does not exist.
@ -263,6 +302,28 @@ class Reconciler():
return False return False
def prune_taps(self):
""" Remove all TAPs from VPP, if they are not in the config. As an exception,
TAPs which are a part of Linux Control Plane, are left alone, to be handled
by prune_lcps() later. """
removed_taps = []
for idx, vpp_tap in self.vpp.cache['taps'].items():
vpp_iface = self.vpp.cache['interfaces'][vpp_tap.sw_if_index]
vpp_ifname = vpp_iface.interface_name
if self.vpp.tap_is_lcp(vpp_ifname):
continue
if self.__tap_has_diff(vpp_ifname):
removed_taps.append(vpp_ifname)
continue
self.logger.debug("TAP OK: %s" % (vpp_ifname))
for ifname in removed_taps:
cli = "delete tap %s" % ifname
self.cli['prune'].append(cli)
self.vpp.cache_remove_tap(ifname)
self.vpp.cache_remove_interface(ifname)
return True
def prune_bondethernets(self): def prune_bondethernets(self):
""" Remove all BondEthernets from VPP, if they are not in the config. If the bond has members, """ Remove all BondEthernets from VPP, if they are not in the config. If the bond has members,
remove those from the bond before removing the bond. """ remove those from the bond before removing the bond. """
@ -338,22 +399,6 @@ class Reconciler():
return True return True
def __tap_is_lcp(self, sw_if_index):
""" Returns True if the given sw_if_index is a TAP interface belonging to an LCP,
or False otherwise."""
if not sw_if_index in self.vpp.cache['interfaces']:
return False
vpp_iface = self.vpp.cache['interfaces'][sw_if_index]
if not vpp_iface.interface_dev_type=="virtio":
return False
match = False
for idx, lcp in self.vpp.cache['lcps'].items():
if vpp_iface.sw_if_index == lcp.host_sw_if_index:
match = True
return match
def prune_sub_interfaces(self): def prune_sub_interfaces(self):
""" Remove interfaces from VPP if they are not in the config, if their encapsulation is different, """ Remove interfaces from VPP if they are not in the config, if their encapsulation is different,
or if the BondEthernet they reside on is different. or if the BondEthernet they reside on is different.
@ -365,7 +410,7 @@ class Reconciler():
if vpp_iface.sub_number_of_tags != numtags: if vpp_iface.sub_number_of_tags != numtags:
continue continue
if self.__tap_is_lcp(vpp_iface.sw_if_index): if self.vpp.tap_is_lcp(vpp_ifname):
continue continue
prune=False prune=False
@ -573,7 +618,7 @@ class Reconciler():
if not ifname in interface.get_interfaces(self.cfg) + loopback.get_loopbacks(self.cfg): if not ifname in interface.get_interfaces(self.cfg) + loopback.get_loopbacks(self.cfg):
vpp_iface = self.vpp.cache['interface_names'][ifname] vpp_iface = self.vpp.cache['interface_names'][ifname]
if self.__tap_is_lcp(vpp_iface.sw_if_index): if self.vpp.tap_is_lcp(ifname):
continue continue
if vpp_iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP if vpp_iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP
@ -596,6 +641,9 @@ class Reconciler():
if not self.create_vxlan_tunnels(): if not self.create_vxlan_tunnels():
self.logger.warning("Could not create VXLAN Tunnels in VPP") self.logger.warning("Could not create VXLAN Tunnels in VPP")
ret = False ret = False
if not self.create_taps():
self.logger.warning("Could not create TAPs in VPP")
ret = False
if not self.create_sub_interfaces(): if not self.create_sub_interfaces():
self.logger.warning("Could not create Sub Interfaces in VPP") self.logger.warning("Could not create Sub Interfaces in VPP")
ret = False ret = False
@ -672,6 +720,29 @@ class Reconciler():
self.cli['create'].append(cli); self.cli['create'].append(cli);
return True return True
def create_taps(self):
for ifname in tap.get_taps(self.cfg):
ifname, iface = tap.get_by_name(self.cfg, ifname)
if ifname in self.vpp.cache['interface_names']:
continue
instance=int(ifname[3:])
cli="create tap id %d host-if-name %s" % (instance, iface['host']['name'])
if 'mac' in iface['host']:
cli+=" host-mac-addr %s" % iface['host']['mac']
if 'namespace' in iface['host']:
cli+=" host-ns %d" % iface['host']['namespace']
if 'bridge' in iface['host']:
cli+=" host-bridge %s" % iface['host']['bridge']
if 'mtu' in iface['host']:
cli+=" host-mtu-size %d" % iface['host']['mtu']
if 'rx-ring-size' in iface:
cli+=" rx-ring-size %d" % iface['rx-ring-size']
if 'tx-ring-size' in iface:
cli+=" tx-ring-size %d" % iface['tx-ring-size']
self.cli['create'].append(cli)
return True
def create_bridgedomains(self): def create_bridgedomains(self):
for ifname in bridgedomain.get_bridgedomains(self.cfg): for ifname in bridgedomain.get_bridgedomains(self.cfg):
ifname, iface = bridgedomain.get_by_name(self.cfg, ifname) ifname, iface = bridgedomain.get_by_name(self.cfg, ifname)

View File

@ -66,7 +66,7 @@ class VPPApi():
self.cache_read = False self.cache_read = False
return {"lcps": {}, "interface_names": {}, "interfaces": {}, "interface_addresses": {}, return {"lcps": {}, "interface_names": {}, "interfaces": {}, "interface_addresses": {},
"bondethernets": {}, "bondethernet_members": {}, "bondethernets": {}, "bondethernet_members": {},
"bridgedomains": {}, "vxlan_tunnels": {}, "l2xcs": {}} "bridgedomains": {}, "vxlan_tunnels": {}, "l2xcs": {}, "taps": {}}
def cache_remove_lcp(self, lcpname): def cache_remove_lcp(self, lcpname):
""" Removes the LCP and TAP interface, identified by lcpname, from the config. """ """ Removes the LCP and TAP interface, identified by lcpname, from the config. """
@ -79,11 +79,10 @@ class VPPApi():
return False return False
ifname = self.cache['interfaces'][lcp.host_sw_if_index].interface_name ifname = self.cache['interfaces'][lcp.host_sw_if_index].interface_name
del self.cache['interface_names'][ifname]
del self.cache['interface_addresses'][lcp.host_sw_if_index]
del self.cache['interfaces'][lcp.host_sw_if_index]
del self.cache['lcps'][lcp.phy_sw_if_index] del self.cache['lcps'][lcp.phy_sw_if_index]
return True
# Remove the TAP interface and its dependencies
return self.cache_remove_interface(ifname)
def cache_remove_bondethernet_member(self, ifname): def cache_remove_bondethernet_member(self, ifname):
""" Removes the bonderthernet member interface, identified by name, from the config. """ """ Removes the bonderthernet member interface, identified by name, from the config. """
@ -106,6 +105,15 @@ class VPPApi():
self.cache['l2xcs'].pop(iface.sw_if_index, None) self.cache['l2xcs'].pop(iface.sw_if_index, None)
return True return True
def cache_remove_tap(self, ifname):
if not ifname in self.cache['interface_names']:
self.logger.warning("Trying to remove a TAP which is not in the config: %s" % ifname)
return False
iface = self.cache['interface_names'][ifname]
self.cache['taps'].pop(iface.sw_if_index, None)
return True
def cache_remove_vxlan_tunnel(self, ifname): def cache_remove_vxlan_tunnel(self, ifname):
if not ifname in self.cache['interface_names']: if not ifname in self.cache['interface_names']:
self.logger.warning("Trying to remove a VXLAN Tunnel which is not in the config: %s" % ifname) self.logger.warning("Trying to remove a VXLAN Tunnel which is not in the config: %s" % ifname)
@ -135,6 +143,8 @@ class VPPApi():
else: else:
del self.cache['bondethernet_members'][iface.sw_if_index] del self.cache['bondethernet_members'][iface.sw_if_index]
self.cache['bondethernets'].pop(iface.sw_if_index, None) self.cache['bondethernets'].pop(iface.sw_if_index, None)
self.cache['taps'].pop(iface.sw_if_index, None)
return True return True
def readconfig(self): def readconfig(self):
@ -201,6 +211,11 @@ class VPPApi():
for l2xc in r: for l2xc in r:
self.cache['l2xcs'][l2xc.rx_sw_if_index] = l2xc self.cache['l2xcs'][l2xc.rx_sw_if_index] = l2xc
self.logger.debug("Retrieving TAPs")
r = self.vpp.api.sw_interface_tap_v2_dump()
for tap in r:
self.cache['taps'][tap.sw_if_index] = tap
self.cache_read = True self.cache_read = True
return self.cache_read return self.cache_read
@ -247,3 +262,18 @@ class VPPApi():
if lcp.phy_sw_if_index == sw_if_index: if lcp.phy_sw_if_index == sw_if_index:
return lcp return lcp
return None return None
def tap_is_lcp(self, tap_ifname):
""" Returns True if the given tap_ifname is a TAP interface belonging to an LCP,
or False otherwise."""
if not tap_ifname in self.cache['interface_names']:
return False
vpp_iface = self.cache['interface_names'][tap_ifname]
if not vpp_iface.interface_dev_type=="virtio":
return False
for idx, lcp in self.cache['lcps'].items():
if vpp_iface.sw_if_index == lcp.host_sw_if_index:
return True
return False