def validate_convert_params(cls, params, also_mandatory=()): for param in ('vpn_instance_id', 'mac_address', 'local_port') + also_mandatory: if param not in params: raise exc.APIMissingParameterException(param) # if local_port is not a dict, then assume it designates a linux # interface if (isinstance(params['local_port'], six.string_types) or isinstance(params['local_port'], six.text_type)): params['local_port'] = {'linuxif': params['local_port']} for param in ('import_rt', 'export_rt'): if param not in params: continue # if import_rt or export_rt are strings, convert them into lists if (isinstance(params[param], six.string_types) or isinstance(params[param], six.text_type)): try: params[param] = re.split(',+ *', params[param]) except Exception: raise exc.APIException("Unable to parse RT string into " " a list: '%s'" % params[param]) # remove duplicates params[param] = list(set(params[param])) if not ('linuxif' in params['local_port'] or 'evpn' in params['local_port']): raise exc.APIException("Mandatory key is missing in local_port " "parameter (linuxif, or evpn)") # validate mac_address try: netaddr.EUI(params['mac_address']) except netaddr.core.AddrFormatError: raise exc.MalformedMACAddress(params['mac_address']) # validate ip_address if present if 'ip_address' in params: try: net = netaddr.IPNetwork(params['ip_address']) params['ip_address_prefix'] = params['ip_address'] params['ip_address_plen'] = net.prefixlen except netaddr.core.AddrFormatError: # Try again assuming ip_address is an IP without a prefix # (implicitly using /32) try: netaddr.IPAddress(params['ip_address']) params['ip_address_prefix'] = params['ip_address'] + "/32" params['ip_address_plen'] = 32 except netaddr.core.AddrFormatError: raise exc.MalformedIPAddress(params['ip_address']) else: params['ip_address_prefix'] = None params['ip_address_plen'] = None if not isinstance(params.get('advertise_subnet', False), bool): raise exc.APIException("'advertise_subnet' must be a boolean")
def _check_instance_type(self, params): if 'vpn_type' not in params: raise exc.APIException("missing instance_type") instance_type = params['vpn_type'] if instance_type not in self.type2class: raise exc.APIException("unknown vpn_type: %s" % instance_type) if instance_type not in self.dataplane_drivers: LOG.error("No dataplane driver for VPN type %s", instance_type) raise exc.APIException("No dataplane driver for VPN type %s" % instance_type) return instance_type
def _raise_if_mac2ip_inconsistency(self, mac_address, ip_address_prefix): if not ip_address_prefix: return if mac_address not in self.ip_address_2_mac[ip_address_prefix]: raise exc.APIException( "Inconsistent endpoint info: IP %s already bound to a MAC " "address different from %s (%s)" % (ip_address_prefix, mac_address, self.ip_address_2_mac[ip_address_prefix]))
def _parse_ipaddress_prefix(self, ip_address_prefix): ip_address = "" mask = 0 try: net = netaddr.IPNetwork(ip_address_prefix) (ip_address, mask) = (str(net.ip), net.prefixlen) except netaddr.core.AddrFormatError: raise exc.APIException("Bogus IP prefix: %s" % ip_address_prefix) return (ip_address, mask)
def validate_directions(self, direction): # by default, assume a driver only supports plugging in both directions # if a driver does not support forwarding only the traffic to the port, # (and hence omit forwarding traffic from the port), it should raise an # exception if directions is TO_PORT # if a driver does not support forwarding only the traffic from the # port (and hence omit forwarding traffic to the port), it should raise # an exception if directions is FROM_PORT if (direction is not None) and (direction != constants.BOTH): self.log.warning("Unsupported direction: %s", direction) raise exc.APIException("Unsupported direction: %s" % direction)
def validate_convert_params(cls, params, also_mandatory=()): for param in ('vpn_instance_id', 'mac_address', 'ip_address', 'local_port') + also_mandatory: if param not in params: raise exc.APIMissingParameterException(param) # if local_port is not a dict, then assume it designates a linux # interface if (isinstance(params['local_port'], six.string_types) or isinstance(params['local_port'], six.text_type)): params['local_port'] = {'linuxif': params['local_port']} # if import_rt or export_rt are strings, convert them into lists for param in ('import_rt', 'export_rt'): if (isinstance(params[param], six.string_types) or isinstance(params[param], six.text_type)): try: params[param] = re.split(',+ *', params[param]) except Exception: raise exc.APIException("Unable to parse RT string into " " a list: '%s'" % params[param]) if not ('linuxif' in params['local_port'] or 'evpn' in params['local_port']): raise exc.APIException("Mandatory key is missing in local_port " "parameter (linuxif, or evpn)") # Verify and format IP address with prefix if necessary if re.match(r'([12]?\d?\d\.){3}[12]?\d?\d\/[123]?\d', params['ip_address']): params['ip_address_prefix'] = params['ip_address'] elif re.match(r'([12]?\d?\d\.){3}[12]?\d?\d', params['ip_address']): params['ip_address_prefix'] = params['ip_address'] + "/32" else: raise exc.MalformedIPAddress() if not isinstance(params.get('advertise_subnet', False), bool): raise exc.APIException("'advertise_subnet' must be a boolean")
def validate_convert_attach_params(cls, params): cls.validate_convert_params(params, also_mandatory=('import_rt', 'export_rt')) params['advertise_subnet'] = params.get('advertise_subnet', False) params['lb_consistent_hash_order'] = params.get( 'lb_consistent_hash_order', 0) params['vni'] = params.get('vni', 0) params['direction'] = params.get('direction') or constants.BOTH if params['direction'] not in constants.ALL_DIRECTIONS: raise exc.APIException("direction should be one of: %s" % ', '.join(constants.ALL_DIRECTIONS)) cls.translate_api_internal(params)
def vif_plugged(self, mac_address, ip_address_prefix, localport, advertise_subnet=False, lb_consistent_hash_order=0, local_pref=None, **kwargs): linuxif = localport['linuxif'] endpoint = (mac_address, ip_address_prefix) # Check if this port has already been plugged # - Verify port informations consistency if mac_address in self.mac_2_localport_data: self.log.debug("MAC address already plugged, checking port " "consistency") pdata = self.mac_2_localport_data[mac_address] if pdata.get("port_info") != localport: raise exc.APIException( "Port information is not consistent. " "MAC address cannot be bound to two " "different ports. Previous plug for " "port %s (%s != %s)" % (linuxif, pdata.get("port_info"), localport)) try: self.endpoint_2_desc[endpoint] = kwargs.get('description') (ip_prefix, plen, refresh_only) = self._check_ip_mac(mac_address, ip_address_prefix, advertise_subnet) self.log.debug("Plugging port (%s)", ip_prefix) pdata = self.mac_2_localport_data.get(mac_address, dict()) if not pdata: pdata['label'] = self.manager.label_allocator.get_new_label( "Incoming traffic for %s, interface %s, endpoint %s" % (self, linuxif, endpoint)) pdata["port_info"] = localport pdata["lb_consistent_hash_order"] = lb_consistent_hash_order direction = kwargs.get('direction') if not refresh_only: self.dp_driver.validate_directions(direction) # Call driver to setup the dataplane for incoming traffic self.dataplane.vif_plugged(mac_address, ip_prefix, localport, pdata['label'], direction) if forward_to_port(direction): endpoint_rd = self._rd_for_endpoint( endpoint, "Route distinguisher for %s, interface %s, endpoint %s" % (self, linuxif, endpoint)) rd = self.instance_rd if plen == 32 else endpoint_rd self.log.info( "Synthesizing and advertising BGP route for VIF " "%s endpoint %s", linuxif, endpoint) route_entry = self.synthesize_vif_bgp_route( mac_address, ip_prefix, plen, pdata['label'], lb_consistent_hash_order, rd, local_pref) self._advertise_route(route_entry) self.endpoint_2_route[endpoint] = route_entry self.endpoint_2_rd[endpoint] = endpoint_rd self.localport_2_endpoints[linuxif].add(endpoint) self.endpoint_2_lp[endpoint] = local_pref self.endpoint_2_direction[endpoint] = direction self.mac_2_localport_data[mac_address] = pdata if ip_address_prefix: self.ip_address_2_mac[ip_address_prefix].add(mac_address) self.mac_2_ips[mac_address].add(ip_address_prefix) # we've a non-wildcard plug, so if a wildcard IP was plugged # for this MAC, consider it replaced: # - withdraw the corresponding route # - cleanup the wildcard endpoint records wildcard_endpoint = (mac_address, None) if wildcard_endpoint in self.localport_2_endpoints[linuxif]: self._withdraw_route( self.endpoint_2_route.pop(wildcard_endpoint)) self.localport_2_endpoints[linuxif].remove( wildcard_endpoint) del self.endpoint_2_desc[wildcard_endpoint] except Exception as e: self.log.error("Error in vif_plugged: %s", e) if linuxif in self.localport_2_endpoints: self.localport_2_endpoints[linuxif].discard(endpoint) if not self.localport_2_endpoints[linuxif]: del self.localport_2_endpoints[linuxif] if mac_address in self.mac_2_localport_data: del self.mac_2_localport_data[mac_address] if ip_address_prefix in self.ip_address_2_mac: self.ip_address_2_mac[ip_address_prefix].discard(mac_address) raise self.log.debug("localport_2_endpoints: %s", self.localport_2_endpoints) self.log.debug("endpoint_2_rd: %s", self.endpoint_2_rd) self.log.debug("mac_2_localport_data: %s", self.mac_2_localport_data) self.log.debug("ip_address_2_mac: %s", self.ip_address_2_mac)
def __init__(self, vpn_manager, dataplane_driver, external_instance_id, instance_id, import_rts, export_rts, gateway_ip, ip_address_plen, readvertise, attract_traffic, fallback=None, **kwargs): self.manager = vpn_manager self.instance_type = self.__class__.__name__ self.instance_id = instance_id self.description = None threading.Thread.__init__(self) self.setDaemon(True) if dataplane_driver.ecmp_support: compare_routes = tracker_worker.compare_ecmp else: compare_routes = tracker_worker.compare_no_ecmp tracker_worker.TrackerWorker.__init__(self, self.manager.bgp_manager, repr(self), compare_routes) lg.LookingGlassLocalLogger.__init__(self, repr(self)) self.lock = threading.RLock() self.import_rts = import_rts self.export_rts = export_rts self.external_instance_id = external_instance_id self.gateway_ip = gateway_ip self.network_plen = ip_address_plen self.fallback = None self.afi = self.__class__.afi self.safi = self.__class__.safi self.dp_driver = dataplane_driver if 'vni' in kwargs: self.instance_label = kwargs.pop('vni') self.forced_vni = True else: self.instance_label = self.manager.label_allocator.get_new_label( "Incoming traffic for %s" % self) self.forced_vni = False self.instance_rd = self.manager.rd_allocator.get_new_rd( "Default route distinguisher for %s" % self) self.localport_data = dict() # One local port -> set of endpoints (MAC and IP addresses tuple) self.localport_2_endpoints = collections.defaultdict(set) # One endpoint (MAC and IP addresses tuple) -> One route distinguisher self.endpoint_2_rd = dict() # endpoint (MAC and IP addresses tuple) -> route entry self.endpoint_2_route = dict() # One MAC address -> One local port self.mac_2_localport_data = dict() # One IP address prefix -> Multiple MAC address self.ip_address_2_mac = collections.defaultdict(set) # One endpoint (MAC and IP addresses tuple) -> BGP local_pref self.endpoint_2_lp = dict() # endpoint (MAC and IP addresses tuple) -> description self.endpoint_2_desc = dict() # endpoint (MAC and IP addresses tuple) -> direction self.endpoint_2_direction = dict() # MAC -> set of IP addresses self.mac_2_ips = collections.defaultdict(set) # Redirected instances list from which traffic is attracted (based on # FlowSpec 5-tuple classification) self.redirected_instances = set() self.dataplane = self.dp_driver.initialize_dataplane_instance( self.instance_id, self.external_instance_id, self.gateway_ip, self.network_plen, self.instance_label, **kwargs) for rt in self.import_rts: self._subscribe(self.afi, self.safi, rt) # Subscribe to FlowSpec routes # FIXME(tmorin): this maybe isn't applicable yet to E-VPN yet self._subscribe(self.afi, exa.SAFI.flow_vpn, rt) if readvertise: self.readvertise = True try: self.readvertise_to_rts = readvertise['to_rt'] except KeyError: raise exc.APIException("'readvertise' specified with no " "'to_rt'") self.readvertise_from_rts = readvertise.get('from_rt', []) self.log.debug("readvertise enabled, from RT:%s, to %s", self.readvertise_from_rts, self.readvertise_to_rts) for rt in self.readvertise_from_rts: self._subscribe(self.afi, self.safi, rt) else: self.log.debug("readvertise not enabled") self.readvertise = False self.attract_static_dest_prefixes = None if (attract_traffic and (self.readvertise or attract_traffic.get('to_rt'))): # Convert route target string to RouteTarget dictionary attract_to_rts = attract_traffic.get('to_rt') if attract_to_rts: converted_to_rts = ( utils.convert_route_targets(attract_to_rts)) if self.readvertise: if converted_to_rts != self.readvertise_to_rts: raise exc.APIException("if both are set, then " "'attract_traffic/to_rt' and " "'readvertise/to_rt' have to " "be equal") else: self.readvertise_to_rts = converted_to_rts try: self.attract_static_dest_prefixes = ( attract_traffic['static_destination_prefixes']) except KeyError: raise exc.APIException("'attract_traffic/to_rt' specified " "without " "'static_destination_prefixes'") if len(self.readvertise_to_rts) != 1: raise exc.APIException("attract_traffic requires exactly one " "RT to be provided in " "'readvertise/to_rt' and " "'attract_traffic/to_rt'") self.attract_traffic = True self.attract_rts = attract_traffic['redirect_rts'] try: self.attract_classifier = attract_traffic['classifier'] except KeyError: raise exc.APIException("'attract_traffic' specified with no " "'classifier'") self.log.debug( "Attract traffic enabled with RT: %s and " "classifier: %s", self.attract_rts, self.attract_classifier) else: self.log.debug("attract traffic not enabled") self.attract_traffic = False self.dataplane.update_fallback(fallback)
def vif_unplugged(self, mac_address, ip_address_prefix, advertise_subnet=False, lb_consistent_hash_order=0): # NOTE(tmorin): move this as a vif_unplugged_precheck, so that # in ipvpn.VRF this is done before readvertised route withdrawal # Verify port and endpoint (MAC address, IP address) tuple consistency pdata = self.mac_2_localport_data.get(mac_address) if (not pdata or (ip_address_prefix in self.ip_address_2_mac and mac_address not in self.ip_address_2_mac[ip_address_prefix]) or (mac_address, ip_address_prefix) not in self.endpoint_2_rd): self.log.error( "vif_unplugged called for endpoint (%s, %s), but " "no consistent informations or was not plugged yet", mac_address, ip_address_prefix) raise exc.APIException("Inconsistent endpoint (%s, %s) info " "or endpoint wasn't plugged yet, " "cannot unplug" % (mac_address, ip_address_prefix)) # Finding label and local port informations label = pdata.get('label') endpoint_rd = self.endpoint_2_rd[(mac_address, ip_address_prefix)] localport = pdata.get('port_info') linuxif = localport['linuxif'] if not label or not localport: self.log.error( "vif_unplugged called for endpoint (%s, %s), but " "port data (%s, %s) is incomplete", mac_address, ip_address_prefix, label, localport) raise Exception("Inconsistent informations for port, bug ?") if linuxif in self.localport_2_endpoints: # Parse address/mask (ip_prefix, plen) = self._parse_ipaddress_prefix(ip_address_prefix) last_endpoint = len(self.localport_2_endpoints[linuxif]) <= 1 if not advertise_subnet and plen != 32: self.log.debug("Using /32 instead of /%d", plen) plen = 32 rd = self.instance_rd if plen == 32 else endpoint_rd self.log.info( "Synthesizing and withdrawing BGP route for VIF %s " "endpoint (%s, %s/%d)", linuxif, mac_address, ip_prefix, plen) route_entry = self.synthesize_vif_bgp_route( mac_address, ip_prefix, plen, label, lb_consistent_hash_order, rd) self._withdraw_route(route_entry) # Unplug endpoint from data plane self.dataplane.vif_unplugged(mac_address, ip_prefix, localport, label, last_endpoint) # Forget data for this port if last endpoint if last_endpoint: self.log.info("Last endpoint, freeing label %s", label) # Free label to the allocator self.manager.label_allocator.release(label) del self.localport_2_endpoints[linuxif] else: self.localport_2_endpoints[linuxif].remove({ 'mac': mac_address, 'ip': ip_address_prefix }) del self.endpoint_2_rd[(mac_address, ip_address_prefix)] # Free route distinguisher to the allocator self.manager.rd_allocator.release(endpoint_rd) if not last_endpoint: if not any([ endpoint['mac'] == mac_address for endpoint in self.localport_2_endpoints[linuxif] ]): del self.mac_2_localport_data[mac_address] else: del self.mac_2_localport_data[mac_address] self.ip_address_2_mac[ip_address_prefix].remove(mac_address) if not self.ip_address_2_mac[ip_address_prefix]: del self.ip_address_2_mac[ip_address_prefix] else: self.log.error( "vif_unplugged called for endpoint {%s, %s}, but" " port data is incomplete", mac_address, ip_address_prefix) raise Exception("bagpipe-bgp bug, check its logs") self.log.info("localport_2_endpoints: %s", self.localport_2_endpoints) self.log.info("mac_2_localport_data: %s", self.mac_2_localport_data) self.log.info("ip_address_2_mac: %s", self.ip_address_2_mac)
def vif_plugged(self, mac_address, ip_address_prefix, localport, advertise_subnet=False, lb_consistent_hash_order=0): linuxif = localport['linuxif'] # Check if this port has already been plugged # - Verify port informations consistency if mac_address in self.mac_2_localport_data: self.log.debug("MAC address already plugged, checking port " "consistency") pdata = self.mac_2_localport_data[mac_address] if pdata.get("port_info") != localport: raise exc.APIException( "Port information is not consistent. " "MAC address cannot be bound to two " "different ports. Previous plug for " "port %s (%s != %s)" % (linuxif, pdata.get("port_info"), localport)) try: # Parse address/mask (ip_prefix, plen) = self._parse_ipaddress_prefix(ip_address_prefix) if not advertise_subnet and plen != 32: self.log.debug("Using /32 instead of /%d", plen) plen = 32 # - Verify (MAC address, IP address) tuple consistency if ip_address_prefix in self.ip_address_2_mac and plen == 32: if mac_address not in self.ip_address_2_mac[ip_address_prefix]: raise exc.APIException("Inconsistent endpoint info: %s " "already bound to a MAC address " "different from %s" % (ip_address_prefix, mac_address)) else: return self.log.debug("Plugging port (%s)", ip_prefix) pdata = self.mac_2_localport_data.get(mac_address, dict()) if not pdata: pdata['label'] = self.manager.label_allocator.get_new_label( "Incoming traffic for %s %d, interface %s" ", endpoint %s/%s" % (self.instance_type, self.instance_id, linuxif, mac_address, ip_address_prefix)) pdata["port_info"] = localport pdata["lb_consistent_hash_order"] = lb_consistent_hash_order endpoint_rd = self.manager.rd_allocator.get_new_rd( "Route distinguisher for %s %d, interface %s, " "endpoint %s/%s" % (self.instance_type, self.instance_id, linuxif, mac_address, ip_address_prefix)) rd = self.instance_rd if plen == 32 else endpoint_rd # Call driver to setup the dataplane for incoming traffic self.dataplane.vif_plugged(mac_address, ip_prefix, localport, pdata['label']) self.log.info( "Synthesizing and advertising BGP route for VIF %s " "endpoint (%s, %s/%d)", linuxif, mac_address, ip_prefix, plen) route_entry = self.synthesize_vif_bgp_route( mac_address, ip_prefix, plen, pdata['label'], lb_consistent_hash_order, rd) self._advertise_route(route_entry) if linuxif not in self.localport_2_endpoints: self.localport_2_endpoints[linuxif] = list() self.localport_2_endpoints[linuxif].append({ 'mac': mac_address, 'ip': ip_address_prefix }) self.endpoint_2_rd[(mac_address, ip_address_prefix)] = endpoint_rd self.mac_2_localport_data[mac_address] = pdata if ip_address_prefix not in self.ip_address_2_mac: self.ip_address_2_mac[ip_address_prefix] = list() self.ip_address_2_mac[ip_address_prefix].append(mac_address) except Exception as e: self.log.error("Error in vif_plugged: %s", e) if linuxif in self.localport_2_endpoints: endpoint = {'mac': mac_address, 'ip': ip_address_prefix} if endpoint in self.localport_2_endpoints[linuxif]: self.localport_2_endpoints[linuxif].remove(endpoint) if not self.localport_2_endpoints[linuxif]: del self.localport_2_endpoints[linuxif] if mac_address in self.mac_2_localport_data: del self.mac_2_localport_data[mac_address] if ip_address_prefix in self.ip_address_2_mac: try: self.ip_address_2_mac[ip_address_prefix].remove( mac_address) except ValueError: # ok, value just had not been added pass raise e self.log.info("localport_2_endpoints: %s", self.localport_2_endpoints) self.log.info("endpoint_2_rd: %s", self.endpoint_2_rd) self.log.info("mac_2_localport_data: %s", self.mac_2_localport_data) self.log.info("ip_address_2_mac: %s", self.ip_address_2_mac)
def __init__(self, vpn_manager, dataplane_driver, external_instance_id, instance_id, import_rts, export_rts, gateway_ip, mask, readvertise, attract_traffic, fallback=None, **kwargs): self.manager = vpn_manager self.instance_type = self.__class__.__name__ self.instance_id = instance_id threading.Thread.__init__(self) self.setDaemon(True) if dataplane_driver.ecmp_support: compare_routes = tracker_worker.compare_ecmp else: compare_routes = tracker_worker.compare_no_ecmp tracker_worker.TrackerWorker.__init__( self, self.manager.bgp_manager, "%s-%d" % (self.instance_type, self.instance_id), compare_routes) lg.LookingGlassLocalLogger.__init__( self, "%s-%d" % (self.instance_type, self.instance_id)) self.lock = threading.Lock() self.import_rts = import_rts self.export_rts = export_rts self.external_instance_id = external_instance_id self.gateway_ip = gateway_ip self.mask = mask self.fallback = None self.afi = self.__class__.afi self.safi = self.__class__.safi assert isinstance(self.afi, exa.AFI) assert isinstance(self.safi, exa.SAFI) self.dp_driver = dataplane_driver if 'vni' in kwargs: self.instance_label = kwargs.pop('vni') self.forced_vni = True else: self.instance_label = self.manager.label_allocator.get_new_label( "Incoming traffic for %s %d" % (self.instance_type, self.instance_id)) self.forced_vni = False self.instance_rd = self.manager.rd_allocator.get_new_rd( "Default route distinguisher for %s %d" % (self.instance_type, self.instance_id)) self.localport_data = dict() # One local port -> List of endpoints (MAC and IP addresses tuple) self.localport_2_endpoints = dict() # One endpoint (MAC and IP addresses tuple) -> One route distinguisher self.endpoint_2_rd = dict() # One MAC address -> One local port self.mac_2_localport_data = dict() # One IP address -> Multiple MAC address self.ip_address_2_mac = dict() # Redirected instances list from which traffic is attracted (based on # FlowSpec 5-tuple classification) self.redirected_instances = set() self.dataplane = self.dp_driver.initialize_dataplane_instance( self.instance_id, self.external_instance_id, self.gateway_ip, self.mask, self.instance_label, **kwargs) for rt in self.import_rts: self._subscribe(self.afi, self.safi, rt) # Subscribe to FlowSpec routes # FIXME(tmorin): this maybe isn't applicable yet to E-VPN yet self._subscribe(self.afi, exa.SAFI(exa.SAFI.flow_vpn), rt) if readvertise: self.readvertise = True try: self.readvertise_to_rts = readvertise['to_rt'] except KeyError: raise exc.APIException("'readvertise' specified with no " "'to_rt'") self.readvertise_from_rts = readvertise.get('from_rt', []) self.log.debug("readvertise enabled, from RT:%s, to %s", self.readvertise_from_rts, self.readvertise_to_rts) for rt in self.readvertise_from_rts: self._subscribe(self.afi, self.safi, rt) else: self.log.debug("readvertise not enabled") self.readvertise = False if self.readvertise and attract_traffic: if len(self.readvertise_to_rts) != 1: raise exc.APIException("attract_traffic requires exactly one " "RT to be provided in " "readvertise/to_rt") self.attract_traffic = True self.attract_rts = attract_traffic['redirect_rts'] try: self.attract_classifier = attract_traffic['classifier'] except KeyError: raise exc.APIException("'attract_traffic' specified with no " "'classifier'") self.log.debug( "Attract traffic enabled with RT: %s and " "classifier: %s", self.attract_rts, self.attract_classifier) else: self.log.debug("attract traffic not enabled") self.attract_traffic = False self.dataplane.update_fallback(fallback)