Add the ability to set any mode/lb on bonds

This requires a schema change, adding 'mode' and 'load-balance' fields,
a semantic invariant that 'load-balance' can only be set in the case of
LACP and XOR bonds, a mapper from the mode/lb strings, ie.
"round-robin" to their VPP numeric counterparts, a bunch of unit tests.

Then in the reconciler, changing bonds (__bond_has_diff()) will
invalidate any LCP or sub-interfaces built on them, so those will have
to be pruned. create_bondethernet() will now create (or re-create)
the bond with the correct flags.

Unit-tests, YAML tests and the integration test all pass.

Updated config-guide.
This commit is contained in:
Pim van Pelt
2022-04-05 15:05:03 +00:00
parent 289138da94
commit 2360d28d0a
11 changed files with 241 additions and 13 deletions

View File

@ -52,6 +52,70 @@ def is_bond_member(yaml, ifname):
return False return False
def get_mode(yaml, ifname):
""" Return the mode of the BondEthernet as a string, defaulting to 'lacp'
if no mode is given. Return None if the bond interface doesn't exist.
Return values: 'round-robin','active-backup','broadcast','lacp','xor'
"""
ifname, iface = get_by_name(yaml, ifname)
if not iface:
return None
if not 'mode' in iface:
return 'lacp'
return iface['mode']
def mode_to_int(mode):
""" Returns the integer representation in VPP of a given bondethernet mode,
or -1 if 'mode' is not a valid string.
See src/vnet/bonding/bond.api and schema.yaml for valid pairs. """
ret = { 'round-robin': 1, 'active-backup': 2, 'xor': 3, 'broadcast': 4, 'lacp': 5 }
try:
return ret[mode]
except:
pass
return -1
def get_lb(yaml, ifname):
""" Return the loadbalance strategy of the BondEthernet as a string. Only
'xor' and 'lacp' modes have loadbalance strategies, so return None if
those modes are not used.
Return values: 'l2', 'l23', 'l34', with 'l34' being the default if
the bond is in xor/lacp mode without a load-balance strategy set
explicitly."""
ifname, iface = get_by_name(yaml, ifname)
if not iface:
return None
mode = get_mode(yaml, ifname)
if not mode in ['xor','lacp']:
return None
if not 'load-balance' in iface:
return 'l34'
return iface['load-balance']
def lb_to_int(lb):
""" Returns the integer representation in VPP of a given load-balance strategy,
or -1 if 'lb' is not a valid string.
See src/vnet/bonding/bond.api and schema.yaml for valid pairs, although
bond.api defined more than we use in vppcfg. """
ret = { 'l2': 0, 'l34': 1, 'l23': 2, 'round-robin': 3, 'broadcast': 4, 'active-backup': 5 }
try:
return ret[lb]
except:
pass
return -1
def validate_bondethernets(yaml): def validate_bondethernets(yaml):
result = True result = True
msgs = [] msgs = []
@ -74,6 +138,9 @@ def validate_bondethernets(yaml):
if instance > 4294967294: if instance > 4294967294:
msgs.append("bondethernet %s has instance %d which is too large" % (ifname, instance)) msgs.append("bondethernet %s has instance %d which is too large" % (ifname, instance))
result = False result = False
if not get_mode(yaml, bond_ifname) in ['xor','lacp'] and 'load-balance' in iface:
msgs.append("bondethernet %s can only have load-balance if in mode XOR or LACP" % (ifname))
result = False
for member in iface['interfaces']: for member in iface['interfaces']:
if (None, None) == interface.get_by_name(yaml, member): if (None, None) == interface.get_by_name(yaml, member):

View File

@ -31,7 +31,34 @@ class TestBondEthernetMethods(unittest.TestCase):
def test_enumerators(self): def test_enumerators(self):
ifs = bondethernet.get_bondethernets(self.cfg) ifs = bondethernet.get_bondethernets(self.cfg)
self.assertEqual(len(ifs), 1) self.assertEqual(len(ifs), 3)
self.assertIn("BondEthernet0", ifs) self.assertIn("BondEthernet0", ifs)
self.assertIn("BondEthernet1", ifs)
self.assertIn("BondEthernet2", ifs)
self.assertNotIn("BondEthernet-noexist", ifs) self.assertNotIn("BondEthernet-noexist", ifs)
def test_get_mode(self):
self.assertEqual('lacp', bondethernet.get_mode(self.cfg, "BondEthernet0"))
self.assertEqual('xor', bondethernet.get_mode(self.cfg, "BondEthernet1"))
def test_mode_to_int(self):
self.assertEqual(1, bondethernet.mode_to_int("round-robin"))
self.assertEqual(2, bondethernet.mode_to_int("active-backup"))
self.assertEqual(3, bondethernet.mode_to_int("xor"))
self.assertEqual(4, bondethernet.mode_to_int("broadcast"))
self.assertEqual(5, bondethernet.mode_to_int("lacp"))
self.assertEqual(-1, bondethernet.mode_to_int("not-exist"))
def test_get_lb(self):
self.assertEqual('l34', bondethernet.get_lb(self.cfg, "BondEthernet0"))
self.assertEqual('l2', bondethernet.get_lb(self.cfg, "BondEthernet1"))
self.assertIsNone(bondethernet.get_lb(self.cfg, "BondEthernet2"))
def test_lb_to_int(self):
self.assertEqual(0, bondethernet.lb_to_int("l2"))
self.assertEqual(1, bondethernet.lb_to_int("l34"))
self.assertEqual(2, bondethernet.lb_to_int("l23"))
self.assertEqual(3, bondethernet.lb_to_int("round-robin"))
self.assertEqual(4, bondethernet.lb_to_int("broadcast"))
self.assertEqual(5, bondethernet.lb_to_int("active-backup"))
self.assertEqual(-1, bondethernet.lb_to_int("not-exist"))

View File

@ -156,20 +156,28 @@ BondEthernets are required to be named `BondEthernetN` (note the camelcase) wher
* ***interfaces***: A list of zero or more interfaces that are bond members. The interfaces * ***interfaces***: A list of zero or more interfaces that are bond members. The interfaces
must be PHYs, and in their `interface` configuration, members are allowed only to set the must be PHYs, and in their `interface` configuration, members are allowed only to set the
MTU. MTU.
* ***mode***: A mode to run the LAG in. Can be one of 'round-robin', 'active-backup', 'xor',
'broadcast' or 'lacp'. The default is LACP.
* ***load-balance***: A loadbalancing strategy to use, if the mode is either XOR or LACP.
Can be one of 'l2', 'l23', or 'l34'. The default is l34, which hashes on the source and
destination IPs and ports.
Note that the configuration object here only specifies the link aggregation and its members. Note that the configuration object here only specifies the link aggregation and its members.
BondEthernets are expected to occur as well in the `interfaces` section, where their sub-interfaces BondEthernets are expected to occur as well in the `interfaces` section, where their sub-interfaces
and IP addresses and so on are specified. and IP addresses and so on are specified.
*Caveat*: Currently, BondEthernets are always created as `LACP` typed devices with a loadbalance
strategy of `l34`. In a future release of `vppcfg`, the type and strategy will be configurable.
Examples: Examples:
``` ```
bondethernets: bondethernets:
BondEthernet0: BondEthernet0:
description: "Core: LACP to fsw0.lab.ipng.ch" description: "Core: LACP to fsw0.lab.ipng.ch"
interfaces: [ GigabitEthernet1/0/0, GigabitEthernet1/0/1 ]
mode: lacp
load-balance: l2
BondEthernet1:
description: "Core: RR LAG"
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ] interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
mode: round-robin
``` ```
### VXLAN Tunnels ### VXLAN Tunnels

View File

@ -1,6 +1,8 @@
bondethernets: bondethernets:
BondEthernet0: BondEthernet0:
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ] interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
mode: xor
load-balance: l2
interfaces: interfaces:
GigabitEthernet3/0/0: GigabitEthernet3/0/0:

View File

@ -1,6 +1,8 @@
bondethernets: bondethernets:
BondEthernet0: BondEthernet0:
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ] interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
mode: lacp
load-balance: l2
interfaces: interfaces:
GigabitEthernet3/0/0: GigabitEthernet3/0/0:

View File

@ -1,6 +1,8 @@
bondethernets: bondethernets:
BondEthernet0: BondEthernet0:
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ] interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
mode: xor
load-balance: l2
interfaces: interfaces:
GigabitEthernet3/0/0: GigabitEthernet3/0/0:

View File

@ -1,6 +1,7 @@
bondethernets: bondethernets:
BondEthernet1: BondEthernet1:
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ] interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
mode: round-robin
interfaces: interfaces:
GigabitEthernet3/0/0: GigabitEthernet3/0/0:

View File

@ -35,6 +35,8 @@ loopback:
bondethernet: bondethernet:
description: str(exclude='\'"',len=64,required=False) description: str(exclude='\'"',len=64,required=False)
interfaces: list(str(matches='.*GigabitEthernet[0-9]+/[0-9]+/[0-9]+')) interfaces: list(str(matches='.*GigabitEthernet[0-9]+/[0-9]+/[0-9]+'))
mode: enum('round-robin','active-backup','broadcast','lacp','xor',required=False)
load-balance: enum('l2','l23','l34',required=False)
--- ---
interface: interface:
description: str(exclude='\'"',len=64,required=False) description: str(exclude='\'"',len=64,required=False)

View File

@ -2,6 +2,15 @@ bondethernets:
BondEthernet0: BondEthernet0:
interfaces: [ GigabitEthernet1/0/0, GigabitEthernet1/0/1 ] interfaces: [ GigabitEthernet1/0/0, GigabitEthernet1/0/1 ]
BondEthernet1:
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
mode: xor
load-balance: l2
BondEthernet2:
interfaces: [ GigabitEthernet4/0/0, GigabitEthernet4/0/1 ]
mode: round-robin
interfaces: interfaces:
GigabitEthernet1/0/0: GigabitEthernet1/0/0:
mtu: 3000 mtu: 3000
@ -14,6 +23,16 @@ interfaces:
100: 100:
mtu: 2000 mtu: 2000
GigabitEthernet3/0/0:
mtu: 3000
GigabitEthernet3/0/1:
mtu: 3000
GigabitEthernet4/0/0:
mtu: 3000
GigabitEthernet4/0/1:
mtu: 3000
BondEthernet0: BondEthernet0:
mtu: 3000 mtu: 3000
lcp: "be012345678" lcp: "be012345678"
@ -22,3 +41,9 @@ interfaces:
100: 100:
mtu: 2000 mtu: 2000
addresses: [ 192.0.2.9/29, 2001:db8:1::1/64 ] addresses: [ 192.0.2.9/29, 2001:db8:1::1/64 ]
BondEthernet1:
mtu: 3000
BondEthernet2:
mtu: 3000

View File

@ -0,0 +1,44 @@
test:
description: "BondEthernet can only have loadbalance if XOR or LACP"
errors:
expected:
- "bondethernet BondEthernet2 can only have load-balance if in mode XOR or LACP"
count: 1
---
bondethernets:
BondEthernet0:
interfaces: [ GigabitEthernet1/0/0, GigabitEthernet1/0/1 ]
mode: xor
load-balance: l34
BondEthernet1:
interfaces: [ GigabitEthernet2/0/0, GigabitEthernet2/0/1 ]
mode: lacp
load-balance: l34
BondEthernet2:
interfaces: [ GigabitEthernet3/0/0, GigabitEthernet3/0/1 ]
mode: round-robin
load-balance: l34
interfaces:
GigabitEthernet1/0/0:
mtu: 3000
GigabitEthernet1/0/1:
mtu: 3000
BondEthernet0:
mtu: 3000
GigabitEthernet2/0/0:
mtu: 3000
GigabitEthernet2/0/1:
mtu: 3000
BondEthernet1:
mtu: 3000
GigabitEthernet3/0/0:
mtu: 3000
GigabitEthernet3/0/1:
mtu: 3000
BondEthernet2:
mtu: 3000

View File

@ -236,6 +236,33 @@ class Reconciler():
self.vpp.cache_remove_l2xc(l2xc) self.vpp.cache_remove_l2xc(l2xc)
return True return True
def __bond_has_diff(self, ifname):
""" Returns True if the given ifname (BondEthernet0) have different attributes,
or if either does not exist.
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]
if not vpp_iface.sw_if_index in self.vpp.cache['bondethernets']:
return True
config_ifname, config_iface = bondethernet.get_by_name(self.cfg, ifname)
if not config_iface:
return True
vpp_bond = self.vpp.cache['bondethernets'][vpp_iface.sw_if_index]
mode = bondethernet.mode_to_int(bondethernet.get_mode(self.cfg, config_ifname))
if mode != vpp_bond.mode:
return True
lb = bondethernet.lb_to_int(bondethernet.get_lb(self.cfg, config_ifname))
if lb != vpp_bond.lb:
return True
return False
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. """
@ -244,7 +271,8 @@ class Reconciler():
for idx, bond in self.vpp.cache['bondethernets'].items(): for idx, bond in self.vpp.cache['bondethernets'].items():
vpp_ifname = bond.interface_name vpp_ifname = bond.interface_name
config_ifname, config_iface = bondethernet.get_by_name(self.cfg, vpp_ifname) config_ifname, config_iface = bondethernet.get_by_name(self.cfg, vpp_ifname)
if not config_iface:
if self.__bond_has_diff(vpp_ifname):
self.prune_addresses(vpp_ifname, []) self.prune_addresses(vpp_ifname, [])
for member in self.vpp.cache['bondethernet_members'][idx]: for member in self.vpp.cache['bondethernet_members'][idx]:
member_ifname = self.vpp.cache['interfaces'][member].interface_name member_ifname = self.vpp.cache['interfaces'][member].interface_name
@ -255,6 +283,7 @@ class Reconciler():
self.cli['prune'].append(cli); self.cli['prune'].append(cli);
removed_interfaces.append(vpp_ifname) removed_interfaces.append(vpp_ifname)
continue continue
for member in self.vpp.cache['bondethernet_members'][idx]: for member in self.vpp.cache['bondethernet_members'][idx]:
member_ifname = self.vpp.cache['interfaces'][member].interface_name member_ifname = self.vpp.cache['interfaces'][member].interface_name
if 'interfaces' in config_iface and not member_ifname in config_iface['interfaces']: if 'interfaces' in config_iface and not member_ifname in config_iface['interfaces']:
@ -326,7 +355,8 @@ class Reconciler():
return match return match
def prune_sub_interfaces(self): def prune_sub_interfaces(self):
""" Remove interfaces from VPP if they are not in the config, or 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.
Start with inner-most (QinQ/QinAD), then Dot1Q/Dot1AD.""" Start with inner-most (QinQ/QinAD), then Dot1Q/Dot1AD."""
removed_interfaces=[] removed_interfaces=[]
for numtags in [ 2, 1 ]: for numtags in [ 2, 1 ]:
@ -338,21 +368,27 @@ class Reconciler():
if self.__tap_is_lcp(vpp_iface.sw_if_index): if self.__tap_is_lcp(vpp_iface.sw_if_index):
continue continue
prune=False
config_ifname, config_iface = interface.get_by_name(self.cfg, vpp_ifname) config_ifname, config_iface = interface.get_by_name(self.cfg, vpp_ifname)
if not config_iface: if not config_iface:
self.prune_addresses(vpp_ifname, []) prune = True
cli="delete sub %s" % (vpp_ifname) elif vpp_iface.interface_dev_type=='bond' and vpp_iface.sub_number_of_tags > 0:
self.cli['prune'].append(cli); config_parent_ifname, config_parent_iface = interface.get_parent_by_name(self.cfg, vpp_ifname)
removed_interfaces.append(vpp_ifname) if self.__bond_has_diff(config_parent_ifname):
continue prune = True
config_encap = interface.get_encapsulation(self.cfg, vpp_ifname) config_encap = interface.get_encapsulation(self.cfg, vpp_ifname)
vpp_encap = self.__get_encapsulation(vpp_iface) vpp_encap = self.__get_encapsulation(vpp_iface)
if config_encap != vpp_encap: if config_encap != vpp_encap:
prune = True
if prune:
self.prune_addresses(vpp_ifname, []) self.prune_addresses(vpp_ifname, [])
cli="delete sub %s" % (vpp_ifname) cli="delete sub %s" % (vpp_ifname)
self.cli['prune'].append(cli); self.cli['prune'].append(cli);
removed_interfaces.append(vpp_ifname) removed_interfaces.append(vpp_ifname)
continue continue
addresses = [] addresses = []
if 'addresses' in config_iface: if 'addresses' in config_iface:
addresses = config_iface['addresses'] addresses = config_iface['addresses']
@ -482,7 +518,7 @@ class Reconciler():
removed_lcps.append(lcp.host_if_name) removed_lcps.append(lcp.host_if_name)
continue continue
if vpp_iface.sub_number_of_tags > 1: if vpp_iface.sub_number_of_tags > 0:
config_encap = interface.get_encapsulation(self.cfg, config_ifname) config_encap = interface.get_encapsulation(self.cfg, config_ifname)
vpp_encap = self.__get_encapsulation(vpp_iface) vpp_encap = self.__get_encapsulation(vpp_iface)
if config_encap != vpp_encap: if config_encap != vpp_encap:
@ -495,6 +531,14 @@ class Reconciler():
if vpp_iface.interface_dev_type=='Loopback': if vpp_iface.interface_dev_type=='Loopback':
## Loopbacks will not have a PHY to check. ## Loopbacks will not have a PHY to check.
continue continue
if vpp_iface.interface_dev_type=='bond':
bond_iface = self.vpp.cache['interfaces'][vpp_iface.sup_sw_if_index]
if self.__bond_has_diff(bond_iface.interface_name):
## If BondEthernet changed, it has to be re-created, so all LCPs must be removed.
cli="lcp delete %s" % (vpp_iface.interface_name)
self.cli['prune'].append(cli);
removed_lcps.append(lcp.host_if_name)
continue
phy_lcp = lcps[vpp_iface.sup_sw_if_index] phy_lcp = lcps[vpp_iface.sup_sw_if_index]
config_phy_ifname, config_phy_iface = interface.get_by_lcp_name(self.cfg, phy_lcp.host_if_name) config_phy_ifname, config_phy_iface = interface.get_by_lcp_name(self.cfg, phy_lcp.host_if_name)
@ -578,7 +622,11 @@ class Reconciler():
continue continue
ifname, iface = bondethernet.get_by_name(self.cfg, ifname) ifname, iface = bondethernet.get_by_name(self.cfg, ifname)
instance = int(ifname[12:]) instance = int(ifname[12:])
cli="create bond mode lacp load-balance l34 id %d" % (instance) mode = bondethernet.get_mode(self.cfg, ifname)
cli="create bond id %d mode %s" % (instance, mode)
lb = bondethernet.get_lb(self.cfg, ifname)
if lb:
cli += " load-balance %s" % lb
self.cli['create'].append(cli); self.cli['create'].append(cli);
return True return True