#!/usr/bin/env python3 # # Copyright 2014-2017 Cumulus Networks, Inc. All rights reserved. # Author: Roopa Prabhu, roopa@cumulusnetworks.com # from ipaddress import IPv4Network, IPv4Address, AddressValueError, ip_address try: import ifupdown2.nlmanager.ipnetwork as ipnetwork import ifupdown2.ifupdown.policymanager as policymanager import ifupdown2.ifupdown.ifupdownflags as ifupdownflags from ifupdown2.lib.addon import Vxlan from ifupdown2.lib.nlcache import NetlinkCacheIfnameNotFoundError from ifupdown2.nlmanager.nlmanager import Link from ifupdown2.ifupdown.iface import ifaceLinkKind, ifaceLinkPrivFlags, ifaceStatus, iface from ifupdown2.ifupdown.utils import utils from ifupdown2.ifupdown.statemanager import statemanager_api as statemanager from ifupdown2.ifupdownaddons.cache import * from ifupdown2.ifupdownaddons.modulebase import moduleBase except (ImportError, ModuleNotFoundError): import nlmanager.ipnetwork as ipnetwork import ifupdown.policymanager as policymanager import ifupdown.ifupdownflags as ifupdownflags from lib.addon import Vxlan from lib.nlcache import NetlinkCacheIfnameNotFoundError from nlmanager.nlmanager import Link from ifupdown.iface import ifaceLinkKind, ifaceLinkPrivFlags, ifaceStatus, iface from ifupdown.utils import utils from ifupdown.statemanager import statemanager_api as statemanager from ifupdownaddons.cache import * from ifupdownaddons.modulebase import moduleBase class vxlan(Vxlan, moduleBase): _modinfo = { "mhelp": "vxlan module configures vxlan interfaces.", "attrs": { "vxlan-id": { "help": "vxlan id", "validrange": ["1", "16777214"], "required": True, "example": ["vxlan-id 100"] }, "vxlan-local-tunnelip": { "help": "vxlan local tunnel ip", "validvals": [""], "example": ["vxlan-local-tunnelip 172.16.20.103"] }, "vxlan-svcnodeip": { "help": "vxlan svc node id", "validvals": [""], "example": ["vxlan-svcnodeip 172.16.22.125"] }, "vxlan-svcnodeip6": { "help": "vxlan svc node ip", "validvals": [""], "example": ["vxlan-svcnodeip6 2001:DB8:8086:6502::"] }, "vxlan-remoteip": { "help": "vxlan remote ip", "validvals": [""], "example": ["vxlan-remoteip 172.16.22.127"], "multiline": True }, "vxlan-learning": { "help": "vxlan learning yes/no", "validvals": ["yes", "no", "on", "off"], "example": ["vxlan-learning no"], "default": "yes" }, "vxlan-ageing": { "help": "vxlan aging timer", "validrange": ["0", "4096"], "example": ["vxlan-ageing 300"], "default": "300" }, "vxlan-purge-remotes": { "help": "vxlan purge existing remote entries", "validvals": ["yes", "no"], "example": ["vxlan-purge-remotes yes"], }, "vxlan-port": { "help": "vxlan UDP port (transmitted to vxlan driver)", "example": ["vxlan-port 4789"], "validrange": ["1", "65536"], "default": "4789", }, "vxlan-physdev": { "help": "vxlan physical device", "example": ["vxlan-physdev eth1"] }, "vxlan-ttl": { "help": "specifies the TTL value to use in outgoing packets " "(range 0..255), 0=auto", "default": "0", "validrange": ["0", "255"], "validvals": ["", "auto"], "example": ['vxlan-ttl 42'], }, "vxlan-tos": { "help": "specifies the ToS value (range 0..255), 1=inherit", "validrange": ["0", "255"], "validvals": ["", "inherit"], "example": ['vxlan-tos 42'], }, "vxlan-mcastgrp": { "help": "vxlan multicast group", "validvals": [""], "example": ["vxlan-mcastgrp 172.16.22.127"], }, "vxlan-mcastgrp6": { "help": "vxlan multicast group", "validvals": [""], "example": ["vxlan-mcastgrp ff02::15c"], }, "vxlan-mcastgrp-map": { "help": "vxlan multicast group for single-vxlan device -" "doesn't support multiline attribute", "example": ["vxlan-mcastgrp-map 1000=239.1.1.100 1001=239.1.1.200"], }, "vxlan-vnifilter": { "help": "vxlan vni filter for single-vxlan device", "validvals": ["on", "off"], "default": "off", "example": ["vxlan-vnifilter yes"], }, "vxlan-remoteip-map": { "help": "static HREP entries for static single vxlan device", "example": ["vxlan-remoteip-map 1000-1002=27.0.0.10-27.0.0.12"], }, "vxlan-udp-csum": { "help": "whether to perform checksumming or not", "validvals": ["yes", "no"], "example": ["vxlan-udp-csum no"] }, "vxlan-vni": { "help": "L3 VxLAN interface (vni list and range are supported)", "validvals": [""], "example": ["vxlan-vni 42"] } } } VXLAN_PHYSDEV_MCASTGRP_DEFAULT = "ipmr-lo" def __init__(self, *args, **kargs): Vxlan.__init__(self) moduleBase.__init__(self, *args, **kargs) self._vxlan_purge_remotes = utils.get_boolean_from_string( policymanager.policymanager_api.get_module_globals( module_name=self.__class__.__name__, attr="vxlan-purge-remotes" ) ) self._vxlan_local_tunnelip = None self._clagd_vxlan_anycast_ip = "" # If mcastgrp is specified we need to rely on a user-configred device (via physdev) # or via a policy variable "vxlan-physdev_mcastgrp". If the device doesn't exist we # create it as a dummy device. We need to keep track of the user configuration to # know when to delete this dummy device (when user remove mcastgrp from it's config) self.vxlan_mcastgrp_ref = False self.vxlan_physdev_mcast = policymanager.policymanager_api.get_module_globals( module_name=self.__class__.__name__, attr="vxlan-physdev-mcastgrp" ) or self.VXLAN_PHYSDEV_MCASTGRP_DEFAULT self.tvd_svd_mix_support = utils.get_boolean_from_string( policymanager.policymanager_api.get_module_globals( module_name=self.__class__.__name__, attr="vxlan-support-mix-dev-types" ), default=True ) self.svd_tvd_errors = {} def reset(self): # in daemon mode we need to reset mcastgrp_ref for every new command # this variable has to be set in get_dependent_ifacenames self.vxlan_mcastgrp_ref = False def syntax_check(self, ifaceobj, ifaceobj_getfunc): if self._is_vxlan_device(ifaceobj): if not ifaceobj.get_attr_value_first('vxlan-local-tunnelip') and not self._vxlan_local_tunnelip: self.logger.warning('%s: missing vxlan-local-tunnelip' % ifaceobj.name) return False self.check_and_raise_svd_tvd_errors(ifaceobj) return self.syntax_check_localip_anycastip_equal( ifaceobj.name, ifaceobj.get_attr_value_first('vxlan-local-tunnelip') or self._vxlan_local_tunnelip, self._clagd_vxlan_anycast_ip ) return True def syntax_check_localip_anycastip_equal(self, ifname, local_ip, anycast_ip): try: if local_ip and anycast_ip and ipnetwork.IPNetwork(local_ip) == ipnetwork.IPNetwork(anycast_ip): self.logger.warning('%s: vxlan-local-tunnelip and clagd-vxlan-anycast-ip are identical (%s)' % (ifname, local_ip)) return False except Exception: pass return True def get_dependent_ifacenames(self, ifaceobj, ifaceobjs_all=None, old_ifaceobjs=False): if ifaceobj.get_attr_value_first("bridge-vlan-vni-map"): ifaceobj.link_privflags |= ifaceLinkPrivFlags.SINGLE_VXLAN if self._is_vxlan_device(ifaceobj): ifaceobj.link_kind |= ifaceLinkKind.VXLAN self._set_global_local_ip(ifaceobj) self.__check_and_tag_l3vxi(ifaceobj) if not old_ifaceobjs and not self.tvd_svd_mix_support: # mixing TVD and SVD is not supported - we need to warn the user # we use a dictionary to make sure to only warn once and prevent each # vxlan from being configured on the system if ifaceobj.link_privflags & ifaceLinkPrivFlags.SINGLE_VXLAN: self.single_vxlan_configured.add(ifaceobj.name) if self.traditional_vxlan_configured: self.svd_tvd_errors[ifaceobj.name] = ( "%s: mixing single-vxlan-device with tradional %s is not supported (TVD: %s)" % (ifaceobj.name, "vxlans" if len(self.traditional_vxlan_configured) > 1 else "vxlan", ", ".join(self.traditional_vxlan_configured)) ) elif ifaceobj.link_privflags & ifaceLinkPrivFlags.L3VXI: pass else: self.traditional_vxlan_configured.add(ifaceobj.name) if self.single_vxlan_configured: self.svd_tvd_errors[ifaceobj.name] = ( "%s: mixing traditional vxlan with single vxlan %s is not supported (SVD: %s)" % (ifaceobj.name, "devices" if len(self.single_vxlan_configured) > 1 else "device", ", ".join(self.single_vxlan_configured)) ) # if we detect a vxlan we check if mcastgrp is set (if so we set vxlan_mcastgrp_ref) # to know when to delete this device. if not self.vxlan_mcastgrp_ref and (ifaceobj.get_attr_value("vxlan-mcastgrp") or ifaceobj.get_attr_value("vxlan-mcastgrp-map")): self.vxlan_mcastgrp_ref = True elif ifaceobj.name == 'lo' and not old_ifaceobjs: clagd_vxlan_list = ifaceobj.get_attr_value('clagd-vxlan-anycast-ip') if clagd_vxlan_list: if len(clagd_vxlan_list) != 1: self.log_warn('%s: multiple clagd-vxlan-anycast-ip lines, using first one' % (ifaceobj.name,)) self._clagd_vxlan_anycast_ip = clagd_vxlan_list[0] self._set_global_local_ip(ifaceobj) # If we should use a specific underlay device for the VXLAN # tunnel make sure this device is set up before the VXLAN iface. physdev = ifaceobj.get_attr_value_first('vxlan-physdev') if physdev: return [physdev] return None def __check_and_tag_l3vxi(self, ifaceobj): if ifaceobj.get_attr_value_first("vxlan-vni"): # to validate the l3vxi interface we need to see the vrf attribute if ifaceobj.get_attr_value_first("vrf"): ifaceobj.link_privflags |= ifaceLinkPrivFlags.L3VXI else: self.logger.warning("%s: l3vxi misconfiguration? missing `vrf` attribute" % ifaceobj.name) def _set_global_local_ip(self, ifaceobj): vxlan_local_tunnel_ip = ifaceobj.get_attr_value_first('vxlan-local-tunnelip') if vxlan_local_tunnel_ip and not self._vxlan_local_tunnelip: self._vxlan_local_tunnelip = vxlan_local_tunnel_ip @staticmethod def _is_vxlan_device(ifaceobj): return ifaceobj.link_kind & ifaceLinkKind.VXLAN \ or ifaceobj.link_privflags & ifaceLinkPrivFlags.SINGLE_VXLAN \ or ifaceobj.link_privflags & ifaceLinkPrivFlags.L3VXI \ or ifaceobj.get_attr_value_first("vxlan-id") \ or ifaceobj.get_attr_value_first("vxlan-vni") \ or ifaceobj.get_attr_value_first("bridge-vlan-vni-map") def __get_vlxan_purge_remotes(self, ifaceobj): if not ifaceobj: return self._vxlan_purge_remotes purge_remotes = ifaceobj.get_attr_value_first('vxlan-purge-remotes') if purge_remotes: purge_remotes = utils.get_boolean_from_string(purge_remotes) else: purge_remotes = self._vxlan_purge_remotes return purge_remotes def get_vxlan_ttl_from_string(self, ttl_config): ttl = 0 if ttl_config: if ttl_config.lower() == "auto": ttl = 0 else: ttl = int(ttl_config) return ttl def get_vxlan_tos_from_string(self, tos_config): if tos_config: if tos_config.lower() == "inherit": return 1 else: return int(tos_config) return None def __config_vxlan_id(self, ifname, ifaceobj, vxlan_id_str, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): """ Get vxlan-id user config and check it's value before inserting it in our netlink dictionary :param ifname: :param ifaceobj: :param vxlan_id_str: :param user_request_vxlan_info_data: :param cached_vxlan_ifla_info_data: :return: """ try: vxlan_id = int(vxlan_id_str) cached_vxlan_id = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_ID) if cached_vxlan_id and cached_vxlan_id != vxlan_id: self.log_error( "%s: Cannot change running vxlan id (%s): Operation not supported" % (ifname, cached_vxlan_id), ifaceobj ) user_request_vxlan_info_data[Link.IFLA_VXLAN_ID] = vxlan_id except ValueError: self.log_error("%s: invalid vxlan-id '%s'" % (ifname, vxlan_id_str), ifaceobj) def __get_vxlan_ageing_int(self, ifname, ifaceobj, link_exists): """ Get vxlan-ageing user config or via policy, return integer value, None or raise on error :param ifname: :param ifaceobj: :param link_exists: :return: """ vxlan_ageing_str = ifaceobj.get_attr_value_first("vxlan-ageing") try: if vxlan_ageing_str: return int(vxlan_ageing_str) vxlan_ageing_str = policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr="vxlan-ageing" ) if not vxlan_ageing_str and link_exists: # if link doesn't exist we let the kernel define ageing vxlan_ageing_str = self.get_attr_default_value("vxlan-ageing") if vxlan_ageing_str: return int(vxlan_ageing_str) except Exception: self.log_error("%s: invalid vxlan-ageing '%s'" % (ifname, vxlan_ageing_str), ifaceobj) def __config_vxlan_ageing(self, ifname, ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): """ Check user config vxlan-ageing and insert it in our netlink dictionary if needed """ vxlan_ageing = self.__get_vxlan_ageing_int(ifname, ifaceobj, link_exists) if not vxlan_ageing or (link_exists and vxlan_ageing == cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_AGEING)): return self.logger.info("%s: set vxlan-ageing %s" % (ifname, vxlan_ageing)) user_request_vxlan_info_data[Link.IFLA_VXLAN_AGEING] = vxlan_ageing def __config_vxlan_port(self, ifname, ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): """ Check vxlan-port user config, validate the integer value and insert it in the netlink dictionary if needed :param ifname: :param ifaceobj: :param link_exists: :param user_request_vxlan_info_data: :param cached_vxlan_ifla_info_data: :return: """ vxlan_port_str = ifaceobj.get_attr_value_first("vxlan-port") try: if not vxlan_port_str: vxlan_port_str = policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr="vxlan-port" ) try: vxlan_port = int(vxlan_port_str) except TypeError: # TypeError means vxlan_port was None # ie: not provided by the user or the policy vxlan_port = self.netlink.VXLAN_UDP_PORT except ValueError as e: self.logger.warning( "%s: vxlan-port: using default %s: invalid configured value %s" % (ifname, self.netlink.VXLAN_UDP_PORT, str(e)) ) vxlan_port = self.netlink.VXLAN_UDP_PORT cached_vxlan_port = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_PORT) if link_exists: if vxlan_port != cached_vxlan_port: self.logger.warning( "%s: vxlan-port (%s) cannot be changed - to apply the desired change please run: ifdown %s && ifup %s" % (ifname, cached_vxlan_port, ifname, ifname) ) return self.logger.info("%s: set vxlan-port %s" % (ifname, vxlan_port)) user_request_vxlan_info_data[Link.IFLA_VXLAN_PORT] = vxlan_port except Exception: self.log_error("%s: invalid vxlan-port '%s'" % (ifname, vxlan_port_str), ifaceobj) def __config_vxlan_tos(self, ifname, ifaceobj, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): """ Get vxlan-tos from user config or policy, validate integer value and insert in netlink dict :param ifname: :param ifaceobj: :param user_request_vxlan_info_data: :param cached_vxlan_ifla_info_data: :return: """ vxlan_tos_str = ifaceobj.get_attr_value_first("vxlan-tos") try: if vxlan_tos_str: vxlan_tos = self.get_vxlan_tos_from_string(vxlan_tos_str) else: vxlan_tos = self.get_vxlan_tos_from_string( policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr="vxlan-tos" ) ) if not vxlan_tos_str: return cached_ifla_vxlan_tos = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_TOS) if vxlan_tos != cached_ifla_vxlan_tos: if cached_ifla_vxlan_tos is not None: self.logger.info("%s: set vxlan-tos %s (cache %s)" % (ifname, vxlan_tos_str if vxlan_tos_str else vxlan_tos, cached_ifla_vxlan_tos)) else: self.logger.info("%s: set vxlan-tos %s" % (ifname, vxlan_tos_str if vxlan_tos_str else vxlan_tos)) user_request_vxlan_info_data[Link.IFLA_VXLAN_TOS] = vxlan_tos except Exception: self.log_error("%s: invalid vxlan-tos '%s'" % (ifname, vxlan_tos_str), ifaceobj) def __config_vxlan_ttl(self, ifname, ifaceobj, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): """ Get vxlan-ttl from user config or policy, validate integer value and insert in netlink dict :param ifname: :param ifaceobj: :param user_request_vxlan_info_data: :param cached_vxlan_ifla_info_data: :return: """ vxlan_ttl_str = ifaceobj.get_attr_value_first("vxlan-ttl") try: if vxlan_ttl_str: vxlan_ttl = self.get_vxlan_ttl_from_string(vxlan_ttl_str) else: vxlan_ttl = self.get_vxlan_ttl_from_string( policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr="vxlan-ttl" ) ) cached_ifla_vxlan_ttl = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_TTL) if vxlan_ttl != cached_ifla_vxlan_ttl: if cached_ifla_vxlan_ttl is not None: self.logger.info("%s: set vxlan-ttl %s (cache %s)" % (ifname, vxlan_ttl_str if vxlan_ttl_str else vxlan_ttl, cached_ifla_vxlan_ttl)) else: self.logger.info("%s: set vxlan-ttl %s" % (ifname, vxlan_ttl_str if vxlan_ttl_str else vxlan_ttl)) user_request_vxlan_info_data[Link.IFLA_VXLAN_TTL] = vxlan_ttl return vxlan_ttl except Exception: self.log_error("%s: invalid vxlan-ttl '%s'" % (ifname, vxlan_ttl_str), ifaceobj) def is_vxlan_on_a_clag_bridge(self, ifaceobj) -> bool: return bool(ifaceobj.link_privflags & ifaceLinkPrivFlags.BRIDGE_PORT and self._clagd_vxlan_anycast_ip and self.is_process_running('clagd')) def __config_vxlan_local_tunnelip(self, ifname, ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): """ Get vxlan-local-tunnelip user config or policy, validate ip address format and insert in netlink dict :param ifname: :param ifaceobj: :param link_exists: :param user_request_vxlan_info_data: :param cached_vxlan_ifla_info_data: :return: """ local = ifaceobj.get_attr_value_first("vxlan-local-tunnelip") if not local and self._vxlan_local_tunnelip: local = self._vxlan_local_tunnelip if link_exists: cached_ifla_vxlan_local = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_LOCAL) # on ifreload do not overwrite anycast_ip to individual ip # if clagd has modified if self._clagd_vxlan_anycast_ip and cached_ifla_vxlan_local: anycastip = ipnetwork.IPNetwork(self._clagd_vxlan_anycast_ip) if ( anycastip == cached_ifla_vxlan_local # there's a change that the cache hasn't been updated in # time to reflect the new anycast ip set by clagd, extra checks: or self.is_vxlan_on_a_clag_bridge(ifaceobj) ): local = cached_ifla_vxlan_local = anycastip self.logger.info("%s: clagd-vxlan-anycast-ip (%s) inherited from loopback interface" % (ifname, local)) else: cached_ifla_vxlan_local = None if not local: local = policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr="vxlan-local-tunnelip" ) if local: try: local = ipnetwork.IPv4Address(local) if local.initialized_with_prefixlen: self.logger.warning("%s: vxlan-local-tunnelip %s: netmask ignored" % (ifname, local)) except Exception as e: raise Exception("%s: invalid vxlan-local-tunnelip %s: %s" % (ifname, local, str(e))) if local: if local != cached_ifla_vxlan_local: self.logger.info("%s: set vxlan-local-tunnelip %s" % (ifname, local)) user_request_vxlan_info_data[Link.IFLA_VXLAN_LOCAL] = local # if both local-ip and anycast-ip are identical the function prints a warning self.syntax_check_localip_anycastip_equal(ifname, local, self._clagd_vxlan_anycast_ip) elif cached_ifla_vxlan_local: self.logger.info("%s: removing vxlan-local-tunnelip (cache %s)" % (ifname, cached_ifla_vxlan_local)) user_request_vxlan_info_data[Link.IFLA_VXLAN_LOCAL] = None return local def __get_vxlan_attribute(self, ifaceobj, attr_name): vxlan_attr_value = ifaceobj.get_attr_value_first(attr_name) if not vxlan_attr_value: vxlan_attr_value = policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr=attr_name ) return vxlan_attr_value def __syntax_check_vxlan_mcast_vni(self, ifaceobj, m, vni): try: int(vni) except ValueError: self.log_error('%s: vxlan-mcastgrp-map "%s" vni format is invalid' % (ifaceobj.name, m)) def __syntax_check_vxlan_mcast_vni_range(self, ifaceobj, m, vni_range): if len(vni_range) != 2: self.log_error('%s: vxlan-mcastgrp-map "%s" vni range format is invalid' % (ifaceobj.name, m)) for vni in vni_range: self.__syntax_check_vxlan_mcast_vni(ifaceobj, m, vni) if int(vni_range[0]) >= int(vni_range[1]): self.log_error('%s: vxlan-mcastgrp-map "%s" vni range is invalid' % (ifaceobj.name, m)) def __syntax_check_vxlan_mcast_grp(self, ifaceobj, m, grp): try: ip = IPv4Address(grp) except AddressValueError: self.log_error('%s: vxlan-mcastgrp-map "%s" group format is invalid' % (ifaceobj.name, m)) if not ip.is_multicast: self.log_error('%s: vxlan-mcastgrp-map "%s" group is not multicast' % (ifaceobj.name, m)) def __syntax_check_vxlan_mcast_grp_range(self, ifaceobj, m, grp_range): if len(grp_range) != 2: self.log_error('%s: vxlan-mcastgrp-map "%s" group format is invalid' % (ifaceobj.name, m)) for grp in grp_range: self.__syntax_check_vxlan_mcast_grp(ifaceobj, m, grp) if int(IPv4Address(grp_range[0])) >= int(IPv4Address(grp_range[1])): self.log_error('%s: vxlan-mcastgrp-map "%s" group range is invalid' % (ifaceobj.name, m)) def __syntax_check_vxlan_mcast_network(self, ifaceobj, m, network, len_vni): try: ip = IPv4Network(network) ip[0] ip[len_vni - 1] except IndexError: self.log_error('%s: vxlan-mcastgrp-map "%s" network range is insufficient' % (ifaceobj.name, m)) except AddressValueError: self.log_error('%s: vxlan-mcastgrp-map "%s" network format is invalid' % (ifaceobj.name, m)) if not ip.is_multicast: self.log_error('%s: vxlan-mcastgrp-map "%s" network is not multicast' % (ifaceobj.name, m)) def __get_vxlan_mcastgrp_map(self, ifaceobj): maps = ifaceobj.get_attr_value('vxlan-mcastgrp-map') if not maps: maps = policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr='vxlan-mcastgrp-map' ) return maps parsed_maps = {} for m_line in maps: # Cover single-line multi-entry case map = m_line.split() for m in map: m_parts = m.split('=') if len(m_parts) != 2: self.log_error('%s: vxlan-mcastgrp-map %s format is invalid' % (ifaceobj.name, m)) vni = m_parts[0] grp = m_parts[1] _range = "-" _network = "/" # One to one mapping case if _range not in vni and _range not in grp: self.__syntax_check_vxlan_mcast_vni(ifaceobj, m, vni) self.__syntax_check_vxlan_mcast_grp(ifaceobj, m, grp) if int(vni) not in parsed_maps: parsed_maps[int(vni)] = IPv4Address(grp) else: self.log_warn('%s: vxlan-mcastgrp-map %s vni %s duplicate' % (ifaceobj.name, vni, m)) # Many VNI case if _range in vni: v_parts = vni.split(_range) self.__syntax_check_vxlan_mcast_vni_range(ifaceobj, m, v_parts) vnis = list(range(int(v_parts[0]), int(v_parts[1]) + 1)) if _range not in grp and _network not in grp: self.__syntax_check_vxlan_mcast_grp(ifaceobj, m, grp) for i in vnis: if i not in parsed_maps: parsed_maps[i] = IPv4Address(grp) else: self.log_warn('%s: vxlan-mcastgrp-map %s vni %s duplicate' % (ifaceobj.name, vni, m)) else: if _network in grp: self.__syntax_check_vxlan_mcast_network(ifaceobj, m, grp, len(vnis)) network = IPv4Network(grp) g_parts = [network[0], network[len(vnis) - 1]] else: g_parts = grp.split(_range) self.__syntax_check_vxlan_mcast_grp_range(ifaceobj, m, g_parts) grp_range = list(range(int(IPv4Address(g_parts[0])), int(IPv4Address(g_parts[1])) + 1)) if len(grp_range) != len(vnis): self.log_error('%s: vxlan-mcastgrp-map "%s" range lengths do not match.' % (ifaceobj.name, m)) for v, g in zip(vnis, grp_range): if v not in parsed_maps: parsed_maps[v] = IPv4Address(g) else: self.log_warn('%s: vxlan-mcastgrp-map %s vni %s duplicate' % (ifaceobj.name, v, m)) return parsed_maps def __config_vxlan_group(self, ifname, ifaceobj, link_exists, mcast_grp, group, physdev, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): """ vxlan-mcastgrp and vxlan-svcnodeip are mutually exclusive this function validates ip format for both attribute and tries to understand what the user really want (remote or group option). :param ifname: :param ifaceobj: :param mcast_grp: :param group: :param physdev: :param user_request_vxlan_info_data: :param cached_vxlan_ifla_info_data: :return: """ if mcast_grp and group: self.log_error("%s: both group (vxlan-mcastgrp %s) and " "remote (vxlan-svcnodeip %s) cannot be specified" % (ifname, mcast_grp, group), ifaceobj) attribute_name = "vxlan-svcnodeip" multicast_group_change = False if group: try: group = ipnetwork.IPv4Address(group) if group.initialized_with_prefixlen: self.logger.warning("%s: vxlan-svcnodeip %s: netmask ignored" % (ifname, group)) except Exception as e: raise Exception("%s: invalid vxlan-svcnodeip %s: %s" % (ifname, group, str(e))) if group.ip.is_multicast: self.logger.warning("%s: vxlan-svcnodeip %s: invalid group address, " "for multicast IP please use attribute \"vxlan-mcastgrp\"" % (ifname, group)) # if svcnodeip is used instead of mcastgrp we warn the user # if mcast_grp is not provided by the user we can instead # use the svcnodeip value if not physdev: self.log_error("%s: vxlan: 'group' (vxlan-mcastgrp) requires 'vxlan-physdev' to be specified" % (ifname)) elif mcast_grp: try: mcast_grp = ipnetwork.IPv4Address(mcast_grp) if mcast_grp.initialized_with_prefixlen: self.logger.warning("%s: vxlan-mcastgrp %s: netmask ignored" % (ifname, mcast_grp)) except Exception as e: raise Exception("%s: invalid vxlan-mcastgrp %s: %s" % (ifname, mcast_grp, str(e))) if not mcast_grp.ip.is_multicast: self.logger.warning("%s: vxlan-mcastgrp %s: invalid group address, " "for non-multicast IP please use attribute \"vxlan-svcnodeip\"" % (ifname, mcast_grp)) # if mcastgrp is specified with a non-multicast address # we warn the user. If the svcnodeip wasn't specified by # the user we can use the mcastgrp value as svcnodeip if not group: group = mcast_grp mcast_grp = None else: attribute_name = "vxlan-mcastgrp" if mcast_grp: group = mcast_grp if not physdev: self.log_error("%s: vxlan: 'group' (vxlan-mcastgrp) requires 'vxlan-physdev' to be specified" % (ifname)) cached_ifla_vxlan_group = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_GROUP) if group != cached_ifla_vxlan_group: if not group: group = ipnetwork.IPNetwork("0.0.0.0") attribute_name = "vxlan-svcnodeip/vxlan-mcastgrp" self.logger.info("%s: set %s %s" % (ifname, attribute_name, group)) user_request_vxlan_info_data[Link.IFLA_VXLAN_GROUP] = group # if the mcastgrp address is changed we need to signal this to the upper function # in this case vxlan needs to be down before applying changes then up'd multicast_group_change = True if link_exists: if cached_ifla_vxlan_group: self.logger.info( "%s: vxlan-mcastgrp configuration changed (cache %s): flapping vxlan device required" % (ifname, cached_ifla_vxlan_group) ) else: self.logger.info( "%s: vxlan-mcastgrp configuration changed: flapping vxlan device required" % ifname ) return group, multicast_group_change def __config_vxlan_group6(self, ifname, ifaceobj, link_exists, mcast_grp, group, physdev, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): """ vxlan-mcastgrp and vxlan-svcnodeip are mutually exclusive this function validates ip format for both attribute and tries to understand what the user really want (remote or group option). :param ifname: :param ifaceobj: :param mcast_grp: :param group: :param physdev: :param user_request_vxlan_info_data: :param cached_vxlan_ifla_info_data: :return: """ if mcast_grp and group: self.log_error("%s: both group (vxlan-mcastgrp6 %s) and " "remote (vxlan-svcnodeip6 %s) cannot be specified" % (ifname, mcast_grp, group), ifaceobj) attribute_name = "vxlan-svcnodeip6" multicast_group_change = False if group: try: group = ipnetwork.IPv6Address(group) except Exception: try: group_ip = ipnetwork.IPv6Network(group).ip self.logger.warning("%s: vxlan-svcnodeip6 %s: netmask ignored" % (ifname, group)) group = group_ip except Exception: raise Exception("%s: invalid vxlan-svcnodeip6 %s: must be in ipv4 format" % (ifname, group)) if group.is_multicast: self.logger.warning("%s: vxlan-svcnodeip6 %s: invalid group address, " "for multicast IP please use attribute \"vxlan-mcastgrp6\"" % (ifname, group)) # if svcnodeip is used instead of mcastgrp we warn the user # if mcast_grp is not provided by the user we can instead # use the svcnodeip value if not physdev: self.log_error("%s: vxlan: 'group' (vxlan-mcastgrp6) requires 'vxlan-physdev' to be specified" % (ifname)) elif mcast_grp: try: mcast_grp = ipnetwork.IPv6Address(mcast_grp) except Exception: try: group_ip = ipnetwork.IPv6Network(mcast_grp).ip self.logger.warning("%s: vxlan-mcastgrp6 %s: netmask ignored" % (ifname, mcast_grp)) mcast_grp = group_ip except Exception: raise Exception("%s: invalid vxlan-mcastgrp6 %s: must be in ipv4 format" % (ifname, mcast_grp)) if not mcast_grp.is_multicast: self.logger.warning("%s: vxlan-mcastgrp6 %s: invalid group address, " "for non-multicast IP please use attribute \"vxlan-svcnodeip6\"" % (ifname, mcast_grp)) # if mcastgrp is specified with a non-multicast address # we warn the user. If the svcnodeip wasn't specified by # the user we can use the mcastgrp value as svcnodeip if not group: group = mcast_grp mcast_grp = None else: attribute_name = "vxlan-mcastgrp6" if mcast_grp: group = mcast_grp if not physdev: self.log_error("%s: vxlan: 'group' (vxlan-mcastgrp6) requires 'vxlan-physdev' to be specified" % (ifname)) cached_ifla_vxlan_group = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_GROUP6) if group != cached_ifla_vxlan_group: if not group: group = ipnetwork.IPNetwork("::0", family=6) attribute_name = "vxlan-svcnodeip6/vxlan-mcastgrp6" self.logger.info("%s: set %s %s" % (ifname, attribute_name, group)) user_request_vxlan_info_data[Link.IFLA_VXLAN_GROUP6] = group # if the mcastgrp address is changed we need to signal this to the upper function # in this case vxlan needs to be down before applying changes then up'd multicast_group_change = True if link_exists: if cached_ifla_vxlan_group: self.logger.info( "%s: vxlan-mcastgrp6 configuration changed (cache %s): flapping vxlan device required" % (ifname, cached_ifla_vxlan_group) ) else: self.logger.info( "%s: vxlan-mcastgrp6 configuration changed: flapping vxlan device required" % ifname ) return group, multicast_group_change def __config_vxlan_learning(self, ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): if not link_exists or not ifaceobj.link_privflags & ifaceLinkPrivFlags.BRIDGE_PORT: vxlan_learning = ifaceobj.get_attr_value_first('vxlan-learning') if not vxlan_learning: vxlan_learning = self.get_attr_default_value('vxlan-learning') vxlan_learning = utils.get_boolean_from_string(vxlan_learning) else: vxlan_learning = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_LEARNING) if vxlan_learning != cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_LEARNING): self.logger.info("%s: set vxlan-learning %s" % (ifaceobj.name, "on" if vxlan_learning else "off")) user_request_vxlan_info_data[Link.IFLA_VXLAN_LEARNING] = vxlan_learning def __config_vxlan_udp_csum(self, ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): vxlan_udp_csum = ifaceobj.get_attr_value_first('vxlan-udp-csum') if not vxlan_udp_csum: vxlan_udp_csum = policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr="vxlan-udp-csum" ) if not vxlan_udp_csum and not link_exists: return if not vxlan_udp_csum: vxlan_udp_csum = self.get_attr_default_value('vxlan-udp-csum') if vxlan_udp_csum: vxlan_udp_csum = utils.get_boolean_from_string(vxlan_udp_csum) else: return if vxlan_udp_csum != cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_UDP_CSUM): self.logger.info("%s: set vxlan-udp-csum %s" % (ifaceobj.name, "on" if vxlan_udp_csum else "off")) user_request_vxlan_info_data[Link.IFLA_VXLAN_UDP_CSUM] = vxlan_udp_csum def __get_vxlan_physdev(self, ifaceobj, mcastgrp, mcastgrp_map): """ vxlan-physdev wrapper, special handling is required for mcastgrp is provided the vxlan needs to use a dummy or real device for tunnel endpoint communication This wrapper will get the physdev from user config or policy. IF the device doesnt exists we create a dummy device. :param ifaceobj: :param mcastgrp: :return physdev: """ physdev = ifaceobj.get_attr_value_first("vxlan-physdev") # if the user provided a physdev we need to honor his config # or if mcastgrp wasn't specified we don't need to go further if physdev or (not mcastgrp and not mcastgrp_map): return physdev physdev = self.vxlan_physdev_mcast if not self.cache.link_exists(physdev): if mcastgrp_map: self.logger.info("%s: needs a dummy device (%s) to use for " "multicast termination (vxlan-mcastgrp-map %s)" % (ifaceobj.name, physdev, mcastgrp)) else: self.logger.info("%s: needs a dummy device (%s) to use for " "multicast termination (vxlan-mcastgrp %s)" % (ifaceobj.name, physdev, mcastgrp)) self.netlink.link_add_with_attributes(ifname=physdev, kind="dummy", ifla={Link.IFLA_MTU: 16000, Link.IFLA_LINKMODE: 1}) self.netlink.link_up(physdev) return physdev def __config_vxlan_physdev(self, link_exists, ifaceobj, vxlan_physdev, user_request_vxlan_info_data, cached_vxlan_ifla_info_data): if vxlan_physdev: try: vxlan_physdev_ifindex = self.cache.get_ifindex(vxlan_physdev) except NetlinkCacheIfnameNotFoundError: try: vxlan_physdev_ifindex = int(self.sysfs.read_file_oneline("/sys/class/net/%s/ifindex" % vxlan_physdev)) except Exception: self.logger.error("%s: physdev %s doesn't exists" % (ifaceobj.name, vxlan_physdev)) return if vxlan_physdev_ifindex != cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_LINK): self.logger.info("%s: set vxlan-physdev %s" % (ifaceobj.name, vxlan_physdev)) user_request_vxlan_info_data[Link.IFLA_VXLAN_LINK] = vxlan_physdev_ifindex # if the vxlan exists we need to return True, meaning that the vxlan # needs to be flapped because we detected a vxlan-physdev change if link_exists: self.logger.info("%s: vxlan-physdev configuration changed: flapping vxlan device required" % ifaceobj.name) return True return False def __get_vxlan_remote_ip_map(self, ifaceobj): attr_name = "vxlan-remoteip-map" maps = ifaceobj.get_attr_value(attr_name) if not maps: maps = policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, attr=attr_name ) if not maps: return {} parsed_maps = {} for m_line in maps: # Cover single-line multi-entry case map = m_line.split() for m in map: m_parts = m.split('=') if len(m_parts) != 2: self.log_error('%s: %s %s format is invalid' % (ifaceobj.name, attr_name, m)) vnis = m_parts[0] _range = "-" remote_ips = [] for config_remote_ip in m_parts[1].split(","): if _range in config_remote_ip: ip_range = config_remote_ip.split("-") try: start = ip_address(ip_range[0]) end = ip_address(ip_range[1]) except Exception as e: self.log_error("%s: %s: invalid ip range '%s': %s" % (ifaceobj.name, attr_name, config_remote_ip, e), ifaceobj) return remote_ips.extend([ipnetwork.ip_address(i) for i in range(int(start), int(end) + 1)]) else: remote_ips.append(ipnetwork.ip_address(config_remote_ip)) # vxlan-remoteip-map 42,84,1000-1005=10.0.0.1,10.0.0.42-45,222.0.0.1-5 # higher priority is the comma for vni in utils.ranges_to_ints(vnis.split(",")) or []: parsed_maps.setdefault(vni, []).extend(remote_ips) return parsed_maps def single_vxlan_device_vni_filter(self, ifaceobj, vxlan_mcast_grp): vnisd = {} for vlan_vni_map in ifaceobj.get_attr_value("bridge-vlan-vni-map"): try: (vls, vis) = utils.get_vlan_vnis_in_map(vlan_vni_map) for v in utils.ranges_to_ints(vis): vnisd[v] = None except Exception as e: self.logger.error("%s: %s (%s)" %(ifaceobj.name, vlan_vni_map, str(e))) return if vxlan_mcast_grp: try: for v, g in vxlan_mcast_grp.items(): if v not in vnisd.keys(): self.logger.error("%s: group %s configured for a vni (%s) not specified in vlan vni map" %(ifaceobj.name, g, v)) return vnisd[v] = str(g) except Exception as e: self.logger.error("%s: %s (%s)" %(ifaceobj.name, vlan_vni_map, str(e))) return vnis_int = utils.ranges_to_ints(vnis) self.iproute2.bridge_link_update_vni_filter(ifaceobj.name, vnisd) def check_and_raise_svd_tvd_errors(self, ifaceobj): err = self.svd_tvd_errors.get(ifaceobj.name) if err: self.log_error(err, ifaceobj) def __get_vxlan_vni_list(self, ifaceobj, string=True): vxlan_vni_str = self.__get_vxlan_attribute(ifaceobj, "vxlan-vni") if vxlan_vni_str: # validate range but return string to be used in bridge vni add cmd vxlan_vni_range = utils.ranges_to_ints(vxlan_vni_str.split()) return vxlan_vni_str if string else vxlan_vni_range return None def _up(self, ifaceobj): self.check_and_raise_svd_tvd_errors(ifaceobj) vxlan_id_str = ifaceobj.get_attr_value_first("vxlan-id") if not ifaceobj.link_privflags & ifaceLinkPrivFlags.SINGLE_VXLAN and not ifaceobj.link_privflags & ifaceLinkPrivFlags.L3VXI and not vxlan_id_str: self.logger.warning("%s: missing vxlan-id attribute on vxlan device" % ifaceobj.name) return ifname = ifaceobj.name link_exists = self.cache.link_exists(ifname) if link_exists: # if link already exists make sure this is a vxlan device_link_kind = self.cache.get_link_kind(ifname) if device_link_kind != "vxlan": self.logger.error( "%s: device already exists and is not a vxlan (type %s)" % (ifname, device_link_kind) ) ifaceobj.set_status(ifaceStatus.ERROR) return # get vxlan running attributes cached_vxlan_ifla_info_data = self.cache.get_link_info_data(ifname) else: cached_vxlan_ifla_info_data = {} user_request_vxlan_info_data = {} if vxlan_id_str: # for single vxlan device we don't have a vxlan-id self.__config_vxlan_id(ifname, ifaceobj, vxlan_id_str, user_request_vxlan_info_data, cached_vxlan_ifla_info_data) self.__config_vxlan_learning(ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data) self.__config_vxlan_ageing(ifname, ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data) self.__config_vxlan_port(ifname, ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data) vxlan_ttl = self.__config_vxlan_ttl(ifname, ifaceobj, user_request_vxlan_info_data, cached_vxlan_ifla_info_data) self.__config_vxlan_tos(ifname, ifaceobj, user_request_vxlan_info_data, cached_vxlan_ifla_info_data) self.__config_vxlan_udp_csum(ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data) local = self.__config_vxlan_local_tunnelip(ifname, ifaceobj, link_exists, user_request_vxlan_info_data, cached_vxlan_ifla_info_data) vxlan_vni = self.__get_vxlan_vni_list(ifaceobj) vxlan_mcast_grp = self.__get_vxlan_attribute(ifaceobj, "vxlan-mcastgrp") vxlan_svcnodeip = self.__get_vxlan_attribute(ifaceobj, "vxlan-svcnodeip") vxlan_mcast_grp6 = self.__get_vxlan_attribute(ifaceobj, "vxlan-mcastgrp6") vxlan_svcnodeip6 = self.__get_vxlan_attribute(ifaceobj, "vxlan-svcnodeip6") vxlan_mcast_grp_map = self.__get_vxlan_mcastgrp_map(ifaceobj) vxlan_physdev = self.__get_vxlan_physdev(ifaceobj, vxlan_mcast_grp, vxlan_mcast_grp_map) vxlan_vnifilter = self.__get_vxlan_attribute(ifaceobj, "vxlan-vnifilter") vxlan_physdev_changed = self.__config_vxlan_physdev( link_exists, ifaceobj, vxlan_physdev, user_request_vxlan_info_data, cached_vxlan_ifla_info_data ) group, multicast_group_changed = self.__config_vxlan_group( ifname, ifaceobj, link_exists, vxlan_mcast_grp, vxlan_svcnodeip, vxlan_physdev, user_request_vxlan_info_data, cached_vxlan_ifla_info_data ) group6, multicast_group_changed6 = self.__config_vxlan_group6( ifname, ifaceobj, link_exists, vxlan_mcast_grp6, vxlan_svcnodeip6, vxlan_physdev, user_request_vxlan_info_data, cached_vxlan_ifla_info_data ) flap_vxlan_device = link_exists and (multicast_group_changed or multicast_group_changed6 or vxlan_physdev_changed) if user_request_vxlan_info_data: if link_exists and len(user_request_vxlan_info_data) == 1 and Link.IFLA_VXLAN_ID in user_request_vxlan_info_data: # if the vxlan already exists it's already cached # user_request_vxlan_info_data always contains at least one # element: vxlan-id self.logger.info('%s: vxlan already exists - no change detected' % ifname) else: if ifaceobj.link_privflags & ifaceLinkPrivFlags.SINGLE_VXLAN: self.iproute2.link_add_single_vxlan( link_exists, ifname, local.ip if local else None, group.ip if group else None, vxlan_physdev, user_request_vxlan_info_data.get(Link.IFLA_VXLAN_PORT), vxlan_vnifilter, vxlan_ttl ) elif ifaceobj.link_privflags & ifaceLinkPrivFlags.L3VXI: self.iproute2.link_add_l3vxi( link_exists, ifname, local.ip if local else None, group.ip if group else None, vxlan_physdev, user_request_vxlan_info_data.get(Link.IFLA_VXLAN_PORT), vxlan_ttl ) else: try: if flap_vxlan_device: self.netlink.link_down_force(ifname) self.netlink.link_add_vxlan_with_info_data(ifname, user_request_vxlan_info_data) if flap_vxlan_device: self.netlink.link_up_force(ifname) except Exception as e: if link_exists: self.log_error("%s: applying vxlan change failed: %s" % (ifname, str(e)), ifaceobj) else: self.log_error("%s: vxlan creation failed: %s" % (ifname, str(e)), ifaceobj) return if ifaceobj.link_privflags & ifaceLinkPrivFlags.L3VXI: add_vni = True if link_exists: running_vxlan_vni_set = set() for obj in json.loads(utils.exec_command("bridge -j -p vni show dev %s" % ifname) or "[]"): for vni_obj in obj.get("vnis", []): start = vni_obj.get("vni") end = vni_obj.get("vniEnd") for vni in utils.ranges_to_ints(["%s-%s" % (start, end if end else start)]): running_vxlan_vni_set.add(vni) if running_vxlan_vni_set != set(utils.ranges_to_ints([vxlan_vni])): self.iproute2.bridge_vni_int_set_del(ifname, running_vxlan_vni_set) else: add_vni = False if add_vni: try: self.iproute2.bridge_vni_add(ifname, vxlan_vni) except Exception as e: self.logger.warning("%s: l3 vxlan vni failure: %s" % (ifname, e)) if ifaceobj.link_privflags & ifaceLinkPrivFlags.SINGLE_VXLAN: if vxlan_vnifilter and utils.get_boolean_from_string(vxlan_vnifilter): self.single_vxlan_device_vni_filter(ifaceobj, vxlan_mcast_grp_map) vxlan_purge_remotes = self.__get_vlxan_purge_remotes(ifaceobj) remoteips = ifaceobj.get_attr_value('vxlan-remoteip') if remoteips: try: for remoteip in remoteips: ipnetwork.IPv4Address(remoteip) except Exception as e: self.log_error('%s: vxlan-remoteip: %s' % (ifaceobj.name, str(e))) # get old remote ips to compare with new user config value and # purge any removed remote ip old_remoteips = self.get_old_remote_ips(ifaceobj.name) if vxlan_purge_remotes or remoteips or (remoteips != old_remoteips): # figure out the diff for remotes and do the bridge fdb updates # only if provisioned by user and not by an vxlan external # controller. local_str = str(local) if local_str and remoteips and local_str in remoteips: remoteips.remove(local_str) peers = self.iproute2.get_vxlan_peers(ifaceobj.name, str(group.ip) if group else None) cur_peers = set(peers) if remoteips: new_peers = set(remoteips) del_list = cur_peers.difference(new_peers) add_list = new_peers.difference(cur_peers) else: del_list = cur_peers add_list = [] for addr in del_list: try: self.iproute2.bridge_fdb_del( ifaceobj.name, "00:00:00:00:00:00", None, True, addr ) except Exception: pass for addr in add_list: try: self.iproute2.bridge_fdb_append( ifaceobj.name, "00:00:00:00:00:00", None, True, addr ) except Exception: pass self.vxlan_remote_ip_map(ifaceobj, vxlan_mcast_grp_map) @staticmethod def get_old_remote_ips(ifname): old_remoteips = [] for old_ifaceobj in statemanager.get_ifaceobjs(ifname) or []: for remote in old_ifaceobj.get_attr_value("vxlan-remoteip") or []: old_remoteips.append(remote) return old_remoteips def vxlan_remote_ip_map(self, ifaceobj, vxlan_mcast_grp_map): # get user configured remote ip map vxlan_remote_ip_map = self.__get_vxlan_remote_ip_map(ifaceobj) or {} # if we have an older config we need to see what needs to be removed # and not check the running state as FRR or other component can add fdb entries old_vxlan_remote_ip_map = {} for old_ifaceobj in statemanager.get_ifaceobjs(ifaceobj.name) or []: old_vxlan_remote_ip_map = {**old_vxlan_remote_ip_map, **self.__get_vxlan_remote_ip_map(old_ifaceobj)} # go through the user config and add new entries while removing existing entries from 'old_vxlan_remote_ip_map' for vni, ips in vxlan_remote_ip_map.items(): for ip in ips: if ip not in old_vxlan_remote_ip_map.get(vni, []): self.iproute2.bridge_fdb_append(ifaceobj.name, "00:00:00:00:00:00", remote=ip, src_vni=vni) else: old_vxlan_remote_ip_map.get(vni, []).remove(ip) # in old_vxlan_remote_ip_map we have the delta between user config and running config. We should delete those # extra fdb entries. First we need to make sure that those are not added by vxlan-mcastgrp-map if old_vxlan_remote_ip_map: for vni, ip in (vxlan_mcast_grp_map or {}).items(): try: old_vxlan_remote_ip_map[vni].remove(ip) except Exception: pass for vni, ips in old_vxlan_remote_ip_map.items(): for ip in ips: try: self.iproute2.bridge_fdb_del_raw(ifaceobj.name, "00:00:00:00:00:00 dst %s src_vni %s" % (ip, vni)) except Exception: pass @staticmethod def get_vxlan_fdb_src_vni(vxlan_mcast_grp_map): fdbs = [] if vxlan_mcast_grp_map: for src_vni, dst_ip in vxlan_mcast_grp_map.items(): fdbs.append(("00:00:00:00:00:00", src_vni, dst_ip)) return fdbs @staticmethod def get_svd_running_fdb(ifname): vxlan_fdb_data = utils.exec_command("bridge fdb show dev %s" % ifname) current_fdb = [] if vxlan_fdb_data: # each entry should look like the following: # 00:00:00:00:00:00 dst 239.1.1.100 src_vni 1000 self permanent for entry in [line for line in vxlan_fdb_data.strip().split("\n") if "src_vni" in line and "00:00:00:00:00:00" in line]: mac, _, dst, _, src_vni = entry.split()[0:5] current_fdb.append((mac, src_vni, dst)) return current_fdb def single_vxlan_device_mcast_grp_map_fdb(self, ifaceobj, ifname, vxlan_mcast_grp_map): # in this piece of code we won't be checking the running state of the fdb table # dumping all fdb entries would cause scalability issues in certain cases. # pulling old mcastgrp-map configuration old_user_config_fdb = [] for old_ifaceobj in statemanager.get_ifaceobjs(ifname) or []: old_user_config_fdb += self.get_vxlan_fdb_src_vni(self.__get_vxlan_mcastgrp_map(old_ifaceobj)) # new user configuration user_config_fdb = self.get_vxlan_fdb_src_vni(vxlan_mcast_grp_map) # compare old and new config to know if we should remove any stale fdb entries. fdb_entries_to_remove = set(old_user_config_fdb) - set(user_config_fdb) if fdb_entries_to_remove: for mac, src_vni, dst_ip in fdb_entries_to_remove: try: self.iproute2.bridge_fdb_del_src_vni(ifname, mac, src_vni) except Exception as e: if "no such file or directory" not in str(e).lower(): self.logger.warning("%s: removing stale fdb entries failed: %s" % (ifname, str(e))) if not user_config_fdb: # if vxlan-mcastgrp-map wasn't configure return return for mac, src_vni, dst_ip in user_config_fdb: try: self.iproute2.bridge_fdb_add_src_vni(ifname, src_vni, dst_ip) except Exception as e: if "file exists" not in str(e).lower(): ifaceobj.set_status(ifaceStatus.ERROR) self.log_error( "%s: vxlan-mcastgrp-map: %s=%s: %s" % (ifname, src_vni, dst_ip, str(e)), raise_error=False ) def single_vxlan_device_mcast_grp_map_vnifilter(self, ifaceobj, ifname, vxlan_mcast_grp_map): # in this piece of code we won't be checking the running state of the fdb table # dumping all fdb entries would cause scalability issues in certain cases. # pulling old mcastgrp-map configuration old_user_config_fdb = [] for old_ifaceobj in statemanager.get_ifaceobjs(ifname) or []: old_user_config_fdb += self.get_vxlan_fdb_src_vni(self.__get_vxlan_mcastgrp_map(old_ifaceobj)) # new user configuration user_config_fdb = self.get_vxlan_fdb_src_vni(vxlan_mcast_grp_map) # compare old and new config to know if we should remove any stale fdb entries. fdb_entries_to_remove = set(old_user_config_fdb) - set(user_config_fdb) self.logger.info(old_user_config_fdb) self.logger.info(user_config_fdb) self.logger.info(fdb_entries_to_remove) if fdb_entries_to_remove: for mac, src_vni, dst_ip in fdb_entries_to_remove: try: self.iproute2.bridge_fdb_del_src_vni(ifname, mac, src_vni) except Exception as e: if "no such file or directory" not in str(e).lower(): self.logger.warning("%s: removing stale fdb entries failed: %s" % (ifname, str(e))) if not user_config_fdb: # if vxlan-mcastgrp-map wasn't configure return return for mac, src_vni, dst_ip in user_config_fdb: try: self.iproute2.bridge_fdb_add_src_vni(ifname, src_vni, dst_ip) except Exception as e: if "file exists" not in str(e).lower(): ifaceobj.set_status(ifaceStatus.ERROR) self.log_error( "%s: vxlan-mcastgrp-map: %s=%s: %s" % (ifname, src_vni, dst_ip, str(e)), raise_error=False ) def _down(self, ifaceobj): try: self.netlink.link_del(ifaceobj.name) except Exception as e: self.log_warn(str(e)) @staticmethod def _query_check_n_update(ifaceobj, ifaceobjcurr, attrname, attrval, running_attrval): if not ifaceobj.get_attr_value_first(attrname): return if running_attrval and attrval == running_attrval: ifaceobjcurr.update_config_with_status(attrname, attrval, 0) else: ifaceobjcurr.update_config_with_status(attrname, running_attrval, 1) @staticmethod def _query_check_n_update_addresses(ifaceobjcurr, attrname, addresses, running_addresses): if addresses: for a in addresses: if a in running_addresses: ifaceobjcurr.update_config_with_status(attrname, a, 0) else: ifaceobjcurr.update_config_with_status(attrname, a, 1) running_addresses = set(running_addresses).difference( set(addresses)) [ifaceobjcurr.update_config_with_status(attrname, a, 1) for a in running_addresses] def _query_check(self, ifaceobj, ifaceobjcurr): ifname = ifaceobj.name if not self.cache.link_exists(ifname): return cached_vxlan_ifla_info_data = self.cache.get_link_info_data(ifname) if not cached_vxlan_ifla_info_data: ifaceobjcurr.check_n_update_config_with_status_many(ifaceobj, self.get_mod_attrs(), -1) return for vxlan_attr_str, vxlan_attr_nl, callable_type in ( ('vxlan-id', Link.IFLA_VXLAN_ID, int), ('vxlan-ttl', Link.IFLA_VXLAN_TTL, int), ('vxlan-tos', Link.IFLA_VXLAN_TOS, int), ('vxlan-port', Link.IFLA_VXLAN_PORT, int), ('vxlan-ageing', Link.IFLA_VXLAN_AGEING, int), ('vxlan-mcastgrp', Link.IFLA_VXLAN_GROUP, ipnetwork.IPv4Address), ('vxlan-mcastgrp6', Link.IFLA_VXLAN_GROUP6, ipnetwork.IPv6Address), ('vxlan-svcnodeip', Link.IFLA_VXLAN_GROUP, ipnetwork.IPv4Address), ('vxlan-svcnodeip6', Link.IFLA_VXLAN_GROUP6, ipnetwork.IPv6Address), ('vxlan-physdev', Link.IFLA_VXLAN_LINK, lambda x: self.cache.get_ifindex(x)), ('vxlan-learning', Link.IFLA_VXLAN_LEARNING, lambda boolean_str: utils.get_boolean_from_string(boolean_str)), ('vxlan-udp-csum', Link.IFLA_VXLAN_UDP_CSUM, lambda boolean_str: utils.get_boolean_from_string(boolean_str)), ): vxlan_attr_value = ifaceobj.get_attr_value_first(vxlan_attr_str) if not vxlan_attr_value: continue cached_vxlan_attr_value = cached_vxlan_ifla_info_data.get(vxlan_attr_nl) try: vxlan_attr_value_nl = callable_type(vxlan_attr_value) except Exception as e: self.logger.warning('%s: %s: %s' % (ifname, vxlan_attr_str, str(e))) ifaceobjcurr.update_config_with_status(vxlan_attr_str, cached_vxlan_attr_value or 'None', 1) continue if vxlan_attr_value_nl == cached_vxlan_attr_value: ifaceobjcurr.update_config_with_status(vxlan_attr_str, vxlan_attr_value, 0) else: ifaceobjcurr.update_config_with_status(vxlan_attr_str, cached_vxlan_attr_value or 'None', 1) # # vxlan-local-tunnelip # running_attrval = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_LOCAL) attrval = ifaceobj.get_attr_value_first('vxlan-local-tunnelip') if not attrval: attrval = self._vxlan_local_tunnelip # TODO: vxlan._vxlan_local_tunnelip should be a ipnetwork.IPNetwork obj ifaceobj.update_config('vxlan-local-tunnelip', attrval) if str(running_attrval) == self._clagd_vxlan_anycast_ip: # if local ip is anycast_ip, then let query_check to go through attrval = self._clagd_vxlan_anycast_ip self._query_check_n_update( ifaceobj, ifaceobjcurr, 'vxlan-local-tunnelip', str(attrval), str(running_attrval.ip) if running_attrval else None ) # # vxlan-remoteip # purge_remotes = self.__get_vlxan_purge_remotes(ifaceobj) if purge_remotes or ifaceobj.get_attr_value('vxlan-remoteip'): # If purge remotes or if vxlan-remoteip's are set # in the config file, we are owners of the installed # remote-ip's, lets check and report any remote ips we don't # understand cached_svcnode = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_GROUP) self._query_check_n_update_addresses( ifaceobjcurr, 'vxlan-remoteip', ifaceobj.get_attr_value('vxlan-remoteip'), self.iproute2.get_vxlan_peers(ifaceobj.name, str(cached_svcnode.ip) if cached_svcnode else None) ) # not ideal but will work for now, l3vxi dev: if ifaceobj.link_privflags & ifaceLinkPrivFlags.L3VXI: user_config_vni_list = set(self.__get_vxlan_vni_list(ifaceobj, string=False)) vxlan_vni_list = set() for obj in json.loads(utils.exec_command("bridge -j -p vni show dev %s" % ifname) or "[]"): for vni_obj in obj.get("vnis", []): start = vni_obj.get("vni") end = vni_obj.get("vniEnd") for vni in utils.ranges_to_ints(["%s-%s" % (start, end if end else start)]): vxlan_vni_list.add(vni) ifaceobjcurr.update_config_with_status( "vxlan-vni", " ".join(utils.compress_into_ranges(vxlan_vni_list)), vxlan_vni_list != user_config_vni_list ) # # vxlan-mcastgrp-map & vxlan-remoteip-map # fdb entries can be added by FRR, so we won't be checking the running # state if there's no record of a user configuration in /e/n/i user_mcastgrp_map = self.__get_vxlan_mcastgrp_map(ifaceobj) user_remote_ip_map = self.__get_vxlan_remote_ip_map(ifaceobj) if not user_mcastgrp_map and not user_remote_ip_map: return fdb_mcast = {} fdb_remote = {} if user_remote_ip_map: for _, src_vni, dst in self.get_svd_running_fdb(ifname): ip = ipnetwork.IPv4Address(dst) if not ip.is_multicast: fdb_remote.setdefault(int(src_vni), []).append(ip) if user_mcastgrp_map: for obj in json.loads(utils.exec_command("bridge -j -p vni show dev %s" % ifname) or "[]"): for vni in obj.get("vnis", []): group = vni.get("group") if not group: continue # we need to reconvert back to ipaddress.IPv4Address because # the existing code uses this type of obj (namely: __get_vxlan_mcastgrp_map) fdb_mcast[vni.get("vni")] = IPv4Address(group) # # vxlan-mcastgrp-map # if not user_mcastgrp_map and fdb_mcast: ifaceobjcurr.update_config_with_status( "vxlan-mcastgrp-map", " ".join(["%s=%s" % (vni, ip) for vni, ip in fdb_mcast.items()]), 1 ) elif user_mcastgrp_map and not fdb_mcast: ifaceobjcurr.update_config_with_status("vxlan-mcastgrp-map", "", 1) elif user_mcastgrp_map or fdb_mcast: ifaceobjcurr.update_config_with_status( "vxlan-mcastgrp-map", " ".join(["%s=%s" % (vni, ip) for vni, ip in fdb_mcast.items()]), user_mcastgrp_map != fdb_mcast ) # # vxlan-remoteip-map # if not user_remote_ip_map and fdb_remote: ifaceobjcurr.update_config_with_status( "vxlan-remoteip-map", " ".join(["%s=%s" % (vni, ",".join(map(str, ips))) for vni, ips in fdb_remote.items()]), 1 ) elif user_remote_ip_map and not fdb_remote: ifaceobjcurr.update_config_with_status("vxlan-remoteip-map", "", 1) elif user_remote_ip_map or fdb_remote: if user_remote_ip_map == fdb_remote: # display the user config with "pass" for config in ifaceobj.get_attr_value("vxlan-remoteip-map"): ifaceobjcurr.update_config_with_status( "vxlan-remoteip-map", config, 0 ) else: # display current running state with ip ranges (but no vni ranges yet) ifaceobjcurr.update_config_with_status( "vxlan-remoteip-map", " ".join(["%s=%s" % (vni, ",".join(utils.compress_into_ip_ranges(ips))) for vni, ips in fdb_remote.items()]), 1 ) def _query_running(self, ifaceobjrunning): ifname = ifaceobjrunning.name if not self.cache.link_exists(ifname): return if self.cache.get_link_kind(ifname) != 'vxlan': return cached_vxlan_ifla_info_data = self.cache.get_link_info_data(ifname) if not cached_vxlan_ifla_info_data: return # # vxlan-id # vxlan_id = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_ID) if not vxlan_id: # no vxlan id, meaning this not a vxlan return ifaceobjrunning.update_config('vxlan-id', str(vxlan_id)) # # vxlan-port # vxlan_port = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_PORT) if vxlan_port: ifaceobjrunning.update_config('vxlan-port', vxlan_port) # # vxlan-svcnode # vxlan_svcnode_value = cached_vxlan_ifla_info_data.get(Link.IFLA_VXLAN_GROUP) if vxlan_svcnode_value: vxlan_svcnode_value = str(vxlan_svcnode_value) ifaceobjrunning.update_config('vxlan-svcnode', vxlan_svcnode_value) # # vxlan-remoteip # purge_remotes = self.__get_vlxan_purge_remotes(None) if purge_remotes: # if purge_remotes is on, it means we own the # remote ips. Query them and add it to the running config attrval = self.iproute2.get_vxlan_peers(ifname, vxlan_svcnode_value) if attrval: [ifaceobjrunning.update_config('vxlan-remoteip', a) for a in attrval] # # vxlan-link # vxlan-ageing # vxlan-learning # vxlan-local-tunnelip # for vxlan_attr_name, vxlan_attr_nl, callable_netlink_value_to_string in ( ('vxlan-physdev', Link.IFLA_VXLAN_LINK, self._get_ifname_for_ifindex), ('vxlan-ageing', Link.IFLA_VXLAN_AGEING, str), ('vxlan-learning', Link.IFLA_VXLAN_LEARNING, lambda value: 'on' if value else 'off'), ('vxlan-udp-csum', Link.IFLA_VXLAN_UDP_CSUM, lambda value: 'on' if value else 'off'), ('vxlan-local-tunnelip', Link.IFLA_VXLAN_LOCAL, str), ): vxlan_attr_value = cached_vxlan_ifla_info_data.get(vxlan_attr_nl) if vxlan_attr_value is not None: vxlan_attr_value_str = callable_netlink_value_to_string(vxlan_attr_value) if vxlan_attr_value: ifaceobjrunning.update_config(vxlan_attr_name, vxlan_attr_value_str) def _get_ifname_for_ifindex(self, ifindex): """ we need this middle-man function to query the cache cache.get_ifname can raise KeyError, we need to catch it and return None """ try: return self.cache.get_ifname(ifindex) except KeyError: return None _run_ops = { "pre-up": _up, "post-down": _down, "query-running": _query_running, "query-checkcurr": _query_check } def get_ops(self): return list(self._run_ops.keys()) def run(self, ifaceobj, operation, query_ifaceobj=None, **extra_args): op_handler = self._run_ops.get(operation) if not op_handler: return if not self._is_vxlan_device(ifaceobj): return if "query" not in operation: if not self.vxlan_mcastgrp_ref \ and self.vxlan_physdev_mcast \ and self.cache.link_exists(self.vxlan_physdev_mcast): self.netlink.link_del(self.vxlan_physdev_mcast) self.reset() if operation == 'query-checkcurr': op_handler(self, ifaceobj, query_ifaceobj) else: op_handler(self, ifaceobj)