Add docstrings, reorder functions. Rename prune_addresses_set_interface_down() to merely prune_interfaces_down(), and make distinction on setting down and pruning addresses, so we don't do it twice.

This commit is contained in:
Pim van Pelt
2022-03-25 11:55:30 +00:00
parent 864c78ddfe
commit 0a31a22e7e
2 changed files with 74 additions and 44 deletions

View File

@ -109,21 +109,21 @@ Fields:
an error is repeated N times, and it's good practice to precisely establish how many errors an error is repeated N times, and it's good practice to precisely establish how many errors
should be expected. That said, this field can be empty or omitted. should be expected. That said, this field can be empty or omitted.
## Reconsiling ## Reconciling
The second important task of this utility is to take the wellformed (validated) configuration and The second important task of this utility is to take the wellformed (validated) configuration and
apply it to the VPP dataplane. The overall flow consists of three phases: apply it to the VPP dataplane. The overall flow consists of three phases:
1. Prune phase (things in VPP that are not in the config), the order is: 1. Prune phase (remove objects from VPP that are not in the config)
1. Create phase (things in the config that are not in VPP), the order is: 1. Create phase (add objects to VPP that are in the config but not VPP)
1. Sync phase, for each interface in the configuration 1. Sync phase, for each object in the configuration
When removing things, care has to be taken to remove inner-most objects first. For example, When removing things, care has to be taken to remove inner-most objects first. For example,
QinQ/QinAD sub-interfaces should be removed before before their intermediary Dot1Q/Dot1AD. Another QinQ/QinAD sub-interfaces should be removed before before their intermediary Dot1Q/Dot1AD. Another
example, MTU of parents should raise before their children, while children should shrink before their example, MTU of parents should raise before their children, while children should shrink before their
parent. Order matters, so first the tool will ensure all items do not have config which they should parent. Order matters, so first the tool will ensure all items do not have config which they should
not, then it will ensure that all items that are not yet present, get created in the right order, not, then it will ensure that all items that are not yet present, get created in the right order,
and finally for all interfaces, they are synchronized with the configuratino (IP addresses, MTU etc). and finally all objects are synchronized with the configuration (IP addresses, MTU etc).
### Pruning ### Pruning
@ -131,7 +131,6 @@ and finally for all interfaces, they are synchronized with the configuratino (IP
1. For any interface that exists in VPP but not in the config: 1. For any interface that exists in VPP but not in the config:
* Starting with QinQ/QinAD, then Dot1Q/Dot1AD, then (BondEthernets, Tunnels, PHYs) * Starting with QinQ/QinAD, then Dot1Q/Dot1AD, then (BondEthernets, Tunnels, PHYs)
* Set it admin-state down * Set it admin-state down
* Remove all of its IP addresses
1. Retrieve all LCP interfaces from VPP, and retrieve their interface information 1. Retrieve all LCP interfaces from VPP, and retrieve their interface information
* Starting with QinQ/QinAD, then Dot1Q/Dot1AD, then (BondEthernets, Tunnels, PHYs) * Starting with QinQ/QinAD, then Dot1Q/Dot1AD, then (BondEthernets, Tunnels, PHYs)
* Remove those that do not exist in the config * Remove those that do not exist in the config

View File

@ -24,7 +24,7 @@ from vpp.vppapi import VPPApi
class Reconciler(): class Reconciler():
def __init__(self, cfg): def __init__(self, cfg):
self.logger = logging.getLogger('vppcfg.vppapi') self.logger = logging.getLogger('vppcfg.reconciler')
self.logger.addHandler(logging.NullHandler()) self.logger.addHandler(logging.NullHandler())
self.vpp = VPPApi() self.vpp = VPPApi()
@ -43,8 +43,44 @@ class Reconciler():
ret = False ret = False
return ret return ret
def prune(self):
""" Remove all objects from VPP that do not occur in the config. For an indepth explanation
of how and why this particular pruning order is chosen, see README.md section on
Reconciling. """
ret = True
if not self.prune_interfaces_down():
self.logger.warning("Could not set interfaces down in VPP")
ret = False
if not self.prune_lcps():
self.logger.warning("Could not prune LCPs from VPP")
ret = False
if not self.prune_loopbacks():
self.logger.warning("Could not prune loopbacks from VPP")
ret = False
if not self.prune_bvis():
self.logger.warning("Could not prune BVIs from VPP")
ret = False
if not self.prune_bridgedomains():
self.logger.warning("Could not prune BridgeDomains from VPP")
ret = False
if not self.prune_l2xcs():
self.logger.warning("Could not prune L2 Cross Connects from VPP")
ret = False
if not self.prune_bondethernets():
self.logger.warning("Could not prune BondEthernets from VPP")
ret = False
if not self.prune_vxlan_tunnels():
self.logger.warning("Could not prune VXLAN Tunnels from VPP")
ret = False
if not self.prune_interfaces():
self.logger.warning("Could not prune interfaces from VPP")
ret = False
return ret
def prune_addresses(self, ifname, address_list): def prune_addresses(self, ifname, address_list):
""" Remove all addresses from interface ifname, except those in address_list """ """ Remove all addresses from interface ifname, except those in address_list,
which may be an empty list, in which case all addresses are removed.
"""
idx = self.vpp.config['interface_names'][ifname].sw_if_index idx = self.vpp.config['interface_names'][ifname].sw_if_index
for a in self.vpp.config['interface_addresses'][idx]: for a in self.vpp.config['interface_addresses'][idx]:
if not a in address_list: if not a in address_list:
@ -52,38 +88,8 @@ class Reconciler():
else: else:
self.logger.debug("Address OK: %s %s" % (ifname, a)) self.logger.debug("Address OK: %s %s" % (ifname, a))
def prune(self):
ret = True
if not self.prune_addresses_set_interface_down():
self.logger.warning("Could not prune addresses and set interfaces down from VPP that are not in the config")
ret = False
if not self.prune_lcps():
self.logger.warning("Could not prune LCPs from VPP that are not in the config")
ret = False
if not self.prune_loopbacks():
self.logger.warning("Could not prune loopbacks from VPP that are not in the config")
ret = False
if not self.prune_bvis():
self.logger.warning("Could not prune BVIs from VPP that are not in the config")
ret = False
if not self.prune_bridgedomains():
self.logger.warning("Could not prune BridgeDomains from VPP that are not in the config")
ret = False
if not self.prune_l2xcs():
self.logger.warning("Could not prune L2 Cross Connects from VPP that are not in the config")
ret = False
if not self.prune_bondethernets():
self.logger.warning("Could not prune BondEthernets from VPP that are not in the config")
ret = False
if not self.prune_vxlan_tunnels():
self.logger.warning("Could not prune VXLAN Tunnels from VPP that are not in the config")
ret = False
if not self.prune_interfaces():
self.logger.warning("Could not prune interfaces from VPP that are not in the config")
ret = False
return ret
def prune_loopbacks(self): def prune_loopbacks(self):
""" Remove loopbacks from VPP, if they do not occur in the config. """
for idx, vpp_iface in self.vpp.config['interfaces'].items(): for idx, vpp_iface in self.vpp.config['interfaces'].items():
if vpp_iface.interface_dev_type!='Loopback': if vpp_iface.interface_dev_type!='Loopback':
continue continue
@ -99,6 +105,7 @@ class Reconciler():
return True return True
def prune_bvis(self): def prune_bvis(self):
""" Remove BVIs (bridge-domain virtual interfaces) from VPP, if they do not occur in the config. """
for idx, vpp_iface in self.vpp.config['interfaces'].items(): for idx, vpp_iface in self.vpp.config['interfaces'].items():
if vpp_iface.interface_dev_type!='BVI': if vpp_iface.interface_dev_type!='BVI':
continue continue
@ -114,6 +121,8 @@ class Reconciler():
return True return True
def prune_bridgedomains(self): def prune_bridgedomains(self):
""" Remove bridge-domains from VPP, if they do not occur in the config. If any interfaces are
found in to-be removed bridge-domains, they are returned to L3 mode, and tag-rewrites removed. """
for idx, bridge in self.vpp.config['bridgedomains'].items(): for idx, bridge in self.vpp.config['bridgedomains'].items():
bridgename = "bd%d" % idx bridgename = "bd%d" % idx
config_ifname, config_iface = bridgedomain.get_by_name(self.cfg, bridgename) config_ifname, config_iface = bridgedomain.get_by_name(self.cfg, bridgename)
@ -136,6 +145,9 @@ class Reconciler():
return True return True
def prune_l2xcs(self): def prune_l2xcs(self):
""" Remove all L2XC source interfaces from VPP, if they do not occur in the config. If they occur,
but are crossconnected to a different interface name, also remove them. Interfaces are put
back into L3 mode, and their tag-rewrites removed. """
for idx, l2xc in self.vpp.config['l2xcs'].items(): for idx, l2xc in self.vpp.config['l2xcs'].items():
vpp_rx_ifname = self.vpp.config['interfaces'][l2xc.rx_sw_if_index].interface_name vpp_rx_ifname = self.vpp.config['interfaces'][l2xc.rx_sw_if_index].interface_name
config_rx_ifname, config_rx_iface = interface.get_by_name(self.cfg, vpp_rx_ifname) config_rx_ifname, config_rx_iface = interface.get_by_name(self.cfg, vpp_rx_ifname)
@ -160,6 +172,8 @@ class Reconciler():
return True 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 those from the bond before removing the bond. """
for idx, bond in self.vpp.config['bondethernets'].items(): for idx, bond in self.vpp.config['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)
@ -180,6 +194,8 @@ class Reconciler():
return True return True
def prune_vxlan_tunnels(self): def prune_vxlan_tunnels(self):
""" Remove all VXLAN Tunnels from VPP, if they are not in the config. If they are in the config
but with differing attributes, remove them also. """
for idx, vpp_vxlan in self.vpp.config['vxlan_tunnels'].items(): for idx, vpp_vxlan in self.vpp.config['vxlan_tunnels'].items():
vpp_ifname = self.vpp.config['interfaces'][idx].interface_name vpp_ifname = self.vpp.config['interfaces'][idx].interface_name
config_ifname, config_iface = vxlan_tunnel.get_by_name(self.cfg, vpp_ifname) config_ifname, config_iface = vxlan_tunnel.get_by_name(self.cfg, vpp_ifname)
@ -199,6 +215,9 @@ class Reconciler():
return True return True
def prune_interfaces(self): def prune_interfaces(self):
""" Remove interfaces from VPP if they are not in the config. Start with inner-most (QinQ/QinAD), then
Dot1Q/Dot1AD, and finally PHY interfaces (which cannot be removed, but their MTU will be set to
the default of 9000, they will have been set down by prune_interfaces_down(). """
for vpp_ifname in self.vpp.get_qinx_interfaces() + self.vpp.get_dot1x_interfaces() + self.vpp.get_phys(): for vpp_ifname in self.vpp.get_qinx_interfaces() + self.vpp.get_dot1x_interfaces() + self.vpp.get_phys():
vpp_iface = self.vpp.config['interface_names'][vpp_ifname] vpp_iface = self.vpp.config['interface_names'][vpp_ifname]
config_ifname, config_iface = interface.get_by_name(self.cfg, vpp_ifname) config_ifname, config_iface = interface.get_by_name(self.cfg, vpp_ifname)
@ -206,8 +225,7 @@ class Reconciler():
if vpp_iface.sub_id > 0: if vpp_iface.sub_id > 0:
self.logger.info("1> delete sub %s" % vpp_ifname) self.logger.info("1> delete sub %s" % vpp_ifname)
else: else:
if vpp_iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP ## Interfaces were sent DOWN in the prune_interfaces_down() step previously
self.logger.info("1> set interface state %s down" % vpp_ifname)
self.prune_addresses(vpp_ifname, []) self.prune_addresses(vpp_ifname, [])
if vpp_iface.link_mtu != 9000: if vpp_iface.link_mtu != 9000:
self.logger.info("1> set interface mtu 9000 %s" % vpp_ifname) self.logger.info("1> set interface mtu 9000 %s" % vpp_ifname)
@ -220,7 +238,9 @@ class Reconciler():
return True return True
def __parent_iface_by_encap(self, sup_sw_if_index, outer, dot1ad=True): def __parent_iface_by_encap(self, sup_sw_if_index, outer, dot1ad=True):
""" Returns the idx of an interface on a given super_sw_if_index with given dot1q/dot1ad outer and inner-dot1q=0 """ """ Returns the sw_if_index of an interface on a given super_sw_if_index with given dot1q/dot1ad outer and inner-dot1q=0,
in other words the intermediary Dot1Q/Dot1AD belonging to a QinX interface. If the interface doesn't exist, None is
returned. """
for idx, iface in self.vpp.config['interfaces'].items(): for idx, iface in self.vpp.config['interfaces'].items():
if iface.sup_sw_if_index != sup_sw_if_index: if iface.sup_sw_if_index != sup_sw_if_index:
continue continue
@ -235,6 +255,8 @@ class Reconciler():
return None return None
def __get_encapsulation(self, iface): def __get_encapsulation(self, iface):
""" Return a dictionary-based encapsulation of the sub-interface, which helps comparing them to the same object
returned by config.interface.get_encapsulation(). """
if iface.sub_if_flags&8: if iface.sub_if_flags&8:
dot1ad = iface.sub_outer_vlan_id dot1ad = iface.sub_outer_vlan_id
dot1q = 0 dot1q = 0
@ -249,6 +271,15 @@ class Reconciler():
"exact-match": bool(exact_match) } "exact-match": bool(exact_match) }
def prune_lcps(self): def prune_lcps(self):
""" Remove LCPs which are not in the configuration, starting with QinQ/QinAD interfaces, then Dot1Q/Dot1AD,
and finally PHYs/BondEthernets/Tunnels/BVIs/Loopbacks. For QinX, special care is taken to ensure that
their intermediary interface exists, and has the correct encalsulation. If the intermediary interface
changed, the QinX LCP is removed. The same is true for Dot1Q/Dot1AD interfaces: if their encapsulation
has changed, we will have to re-create the underlying sub-interface, so the LCP has to be removed.
Order is important: destroying an LCP of a PHY will invalidate its Dot1Q/Dot1AD as well as their
downstream children in Linux.
"""
lcps = self.vpp.config['lcps'] lcps = self.vpp.config['lcps']
## Remove LCPs for QinX interfaces ## Remove LCPs for QinX interfaces
@ -373,13 +404,13 @@ class Reconciler():
self.logger.debug("LCP OK: %s -> (vpp=%s, config=%s)" % (lcp.host_if_name, vpp_iface.interface_name, config_ifname)) self.logger.debug("LCP OK: %s -> (vpp=%s, config=%s)" % (lcp.host_if_name, vpp_iface.interface_name, config_ifname))
return True return True
def prune_addresses_set_interface_down(self): def prune_interfaces_down(self):
""" Set admin-state down for all interfaces that are not in the config. """
for ifname in self.vpp.get_qinx_interfaces() + self.vpp.get_dot1x_interfaces() + self.vpp.get_bondethernets() + self.vpp.get_vxlan_tunnels() + self.vpp.get_phys(): for ifname in self.vpp.get_qinx_interfaces() + self.vpp.get_dot1x_interfaces() + self.vpp.get_bondethernets() + self.vpp.get_vxlan_tunnels() + self.vpp.get_phys():
if not ifname in interface.get_interfaces(self.cfg): if not ifname in interface.get_interfaces(self.cfg):
iface = self.vpp.config['interface_names'][ifname] iface = self.vpp.config['interface_names'][ifname]
if iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP if iface.flags & 1: # IF_STATUS_API_FLAG_ADMIN_UP
self.logger.info("1> set interface state %s down" % ifname) self.logger.info("1> set interface state %s down" % ifname)
self.prune_addresses(ifname, [])
return True return True