Source code for faucet.valve_switch_stack

"""Manage flooding/learning on stacked datapaths."""

# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import copy

from faucet import valve_of
from faucet.valve_switch_standalone import ValveSwitchManager
from faucet.vlan import NullVLAN


[docs] class ValveSwitchStackManagerBase(ValveSwitchManager): """Base class for dataplane based flooding/learning on stacked dataplanes.""" # By default, no reflection used for flooding algorithms. _USES_REFLECTION = False def __init__(self, stack_manager, **kwargs): super().__init__(**kwargs) self.stack_manager = stack_manager self._set_ext_port_flag = () self._set_nonext_port_flag = () self.external_root_only = False if self.has_externals: self.logger.info("external ports present, using loop protection") self._set_ext_port_flag = ( self.flood_table.set_external_forwarding_requested(), ) self._set_nonext_port_flag = ( self.flood_table.set_no_external_forwarding_requested(), ) if ( not self.stack_manager.stack.is_root() and self.stack_manager.stack.is_root_candidate() ): self.logger.info("external flooding on root only") self.external_root_only = True @staticmethod def _non_stack_learned(other_valves, pkt_meta): """ Obtain DP that has learnt the host that sent the packet Args: other_valves (list): Other valves pkt_meta (PacketMeta): Packet meta sent by the host Returns: DP: DP that has learnt the host """ other_local_dp_entries = [] other_external_dp_entries = [] vlan_vid = pkt_meta.vlan.vid for other_valve in other_valves: other_dp_vlan = other_valve.dp.vlans.get(vlan_vid, None) if other_dp_vlan is not None: entry = other_dp_vlan.cached_host(pkt_meta.eth_src) if not entry: continue if not entry.port.non_stack_forwarding(): continue if entry.port.loop_protect_external: other_external_dp_entries.append(other_valve.dp) else: other_local_dp_entries.append(other_valve.dp) # Another DP has learned locally, has priority. if other_local_dp_entries: return other_local_dp_entries[0] # No other DP has learned locally, but at least one has learned externally. if other_external_dp_entries: entry = pkt_meta.vlan.cached_host(pkt_meta.eth_src) # This DP has not learned the host either, use other's external. if entry is None: return other_external_dp_entries[0] return None def _external_forwarding_requested(self, port): external_forwarding_requested = None if self.has_externals: if port.tagged_vlans and port.loop_protect_external: external_forwarding_requested = False elif not port.stack: external_forwarding_requested = True return external_forwarding_requested def _build_flood_acts_for_port( self, vlan, exclude_unicast, port, exclude_all_external=False, exclude_restricted_bcast_arpnd=False, ): # pylint: disable=too-many-arguments,too-many-positional-arguments if self.external_root_only: exclude_all_external = True return super()._build_flood_acts_for_port( vlan, exclude_unicast, port, exclude_all_external=exclude_all_external, exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd, ) def _flood_actions( self, in_port, external_ports, away_flood_actions, toward_flood_actions, local_flood_actions, ): # pylint: disable=too-many-arguments,too-many-positional-arguments raise NotImplementedError def _build_flood_rule_actions( self, vlan, exclude_unicast, in_port, exclude_all_external=False, exclude_restricted_bcast_arpnd=False, ): # pylint: disable=too-many-arguments """Compiles all the possible flood rule actions for a port on a stack node""" exclude_ports = list(self.stack_manager.inactive_away_ports) external_ports = vlan.loop_protect_external_ports() if in_port and self.stack_manager.is_stack_port(in_port): in_port_peer_dp = in_port.stack["dp"] exclude_ports = exclude_ports + self.stack_manager.adjacent_stack_ports( in_port_peer_dp ) local_flood_actions = tuple( self._build_flood_local_rule_actions( vlan, exclude_unicast, in_port, exclude_all_external, exclude_restricted_bcast_arpnd, ) ) away_flood_actions = tuple( valve_of.flood_tagged_port_outputs( self.stack_manager.away_ports, in_port, exclude_ports=exclude_ports ) ) toward_flood_actions = tuple( valve_of.flood_tagged_port_outputs( self.stack_manager.chosen_towards_ports, in_port ) ) flood_acts = self._flood_actions( in_port, external_ports, away_flood_actions, toward_flood_actions, local_flood_actions, ) return flood_acts def _build_mask_flood_rules_filters( self, port, vlan, eth_dst, eth_dst_mask, prune ): # pylint: disable=too-many-arguments,too-many-positional-arguments """Builds filter for the input table to filter packets on ports that are pruned""" ofmsgs = [] match = {"in_port": port.number, "vlan": vlan} if eth_dst is not None: match.update({"eth_dst": eth_dst, "eth_dst_mask": eth_dst_mask}) replace_priority_offset = self.classification_offset - ( self.pipeline.filter_priority - self.pipeline.select_priority ) priority_offset = replace_priority_offset if eth_dst is None: priority_offset -= 1 if prune: # Allow the prune rule to be replaced with OF strict matching if # this port is unpruned later. ofmsgs.extend( self.pipeline.filter_packets(match, priority_offset=priority_offset) ) else: ofmsgs.extend( self.pipeline.remove_filter(match, priority_offset=priority_offset) ) # Control learning from multicast/broadcast on non-root DPs. if ( not self.stack_manager.stack.is_root() and eth_dst is not None and self._USES_REFLECTION ): # If this is an edge DP, we don't have to learn from # hosts that only broadcast. If we're an intermediate # DP, only learn from broadcasts further away from # the root (and ignore the reflected broadcast for # learning purposes). if ( self.stack_manager.stack.is_edge() or self.stack_manager.is_towards_root(port) ): ofmsgs.extend( self.pipeline.select_packets( self.flood_table, match, priority_offset=self.classification_offset, ) ) return ofmsgs def _build_mask_flood_rules_flood_acts( self, vlan, eth_type, eth_dst, eth_dst_mask, exclude_unicast, exclude_restricted_bcast_arpnd, command, cold_start, prune, port, ): # pylint: disable=unused-argument,too-many-arguments,too-many-positional-arguments """Builds the flood rules for the flood table to forward packets along the stack topology""" ofmsgs = [] flood_acts = [] if self.has_externals: # If external flag is set, flood to external ports, otherwise exclude them. for ext_port_flag, exclude_all_external in ( (valve_of.PCP_NONEXT_PORT_FLAG, True), (valve_of.PCP_EXT_PORT_FLAG, False), ): if not prune: flood_acts, _, _ = self._build_flood_acts_for_port( vlan, exclude_unicast, port, exclude_all_external=exclude_all_external, exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd, ) port_flood_ofmsg = self._build_flood_rule_for_port( vlan, eth_type, eth_dst, eth_dst_mask, command, port, flood_acts, add_match={valve_of.EXTERNAL_FORWARDING_FIELD: ext_port_flag}, ) ofmsgs.append(port_flood_ofmsg) else: if not prune: flood_acts, _, _ = self._build_flood_acts_for_port( vlan, exclude_unicast, port, exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd, ) port_flood_ofmsg = self._build_flood_rule_for_port( vlan, eth_type, eth_dst, eth_dst_mask, command, port, flood_acts ) ofmsgs.append(port_flood_ofmsg) return ofmsgs def _build_mask_flood_rules( self, vlan, eth_type, eth_dst, eth_dst_mask, exclude_unicast, exclude_restricted_bcast_arpnd, command, cold_start, ): # pylint: disable=too-many-arguments,too-many-positional-arguments """Builds that flood rules for each mask for each port in the stack. This takes into account the pruned and non-pruned ports and returns the appropriate flood rule actions""" # Stack ports aren't in VLANs, so need special rules to cause flooding from them. ofmsgs = super()._build_mask_flood_rules( vlan, eth_type, eth_dst, eth_dst_mask, exclude_unicast, exclude_restricted_bcast_arpnd, command, cold_start, ) for port in self.stack_manager.stack_ports(): if eth_dst is not None: # Prune broadcast flooding where multiply connected to same DP prune = self.stack_manager.is_pruned_port(port) else: # Do not prune unicast, may be reply from directly connected DP. prune = False ofmsgs.extend( self._build_mask_flood_rules_filters( port, vlan, eth_dst, eth_dst_mask, prune ) ) ofmsgs.extend( self._build_mask_flood_rules_flood_acts( vlan, eth_type, eth_dst, eth_dst_mask, exclude_unicast, exclude_restricted_bcast_arpnd, command, cold_start, prune, port, ) ) return ofmsgs
[docs] def edge_learn_port(self, other_valves, pkt_meta): """ Find a port towards the edge DP where the packet originated from Args: other_valves (list): All Valves other than this one. pkt_meta (PacketMeta): PacketMeta instance for packet received. Returns: port to learn host on, or None. """ # Got a packet from another DP. if pkt_meta.port.stack: # Received packet from edge_dp = self._edge_dp_for_host(other_valves, pkt_meta) if edge_dp: return self.stack_manager.edge_learn_port_towards(pkt_meta, edge_dp) # Assuming no DP has learned this host. return None # Got a packet locally. # If learning on an external port, check another DP hasn't # already learned on a local/non-external port. if pkt_meta.port.loop_protect_external: edge_dp = self._non_stack_learned(other_valves, pkt_meta) if edge_dp: return self.stack_manager.edge_learn_port_towards(pkt_meta, edge_dp) # Locally learn. return super().edge_learn_port(other_valves, pkt_meta)
def _edge_dp_for_host(self, other_valves, pkt_meta): """Simple distributed unicast learning. Args: other_valves (list): All Valves other than this one. pkt_meta (PacketMeta): PacketMeta instance for packet received. Returns: Valve instance or None (of edge datapath where packet received) """ raise NotImplementedError
[docs] def add_drop_spoofed_faucet_mac_rules(self, vlan): """Install rules to drop spoofed faucet mac""" # antispoof for FAUCET's MAC address # TODO: antispoof for controller IPs on this VLAN, too. ofmsgs = [] if self.drop_spoofed_faucet_mac: for port in self.ports.values(): if not port.stack: ofmsgs.extend( self.pipeline.filter_packets( {"eth_src": vlan.faucet_mac, "in_port": port.number} ) ) return ofmsgs
[docs] def add_port(self, port): ofmsgs = super().add_port(port) # If this is a stacking port, accept all VLANs (came from another FAUCET) if port.stack: # Actual stack traffic will have VLAN tags. ofmsgs.append( self.vlan_table.flowdrop( match=self.vlan_table.match(in_port=port.number, vlan=NullVLAN()), priority=self.low_priority + 1, ) ) ofmsgs.append( self.vlan_table.flowmod( match=self.vlan_table.match(in_port=port.number), priority=self.low_priority, inst=self.pipeline.accept_to_classification(), ) ) return ofmsgs
[docs] def del_port(self, port): ofmsgs = super().del_port(port) if port.stack: for vlan in self.vlans.values(): vlan.clear_cache_hosts_on_port(port) ofmsgs.extend(self._del_host_flows(port)) return ofmsgs
[docs] def get_lacp_dpid_nomination(self, lacp_id, valve, other_valves): """Chooses the DP for a given LAG. The DP will be nominated by the following conditions in order: 1) Number of LAG ports 2) Root DP 3) Lowest DPID Args: lacp_id: The LACP LAG ID other_valves (list): list of other valves Returns: nominated_dpid, reason """ if not other_valves: return None, "" stacked_other_valves = self.stack_manager.stacked_valves(other_valves) all_stacked_valves = {valve}.union(stacked_other_valves) ports = {} no_sync_ports = {} root_dpid = None for stack_valve in all_stacked_valves: all_lags = stack_valve.dp.lags_up() if lacp_id in all_lags: ports[stack_valve.dp.dp_id] = len(all_lags[lacp_id]) nosync_lags = stack_valve.dp.lags_nosync() for l_id in nosync_lags: ports.setdefault(stack_valve.dp.dp_id, 0) no_sync_ports[stack_valve.dp.dp_id] = len(nosync_lags.get(l_id, 0)) if stack_valve.dp.stack.is_root(): root_dpid = stack_valve.dp.dp_id # Order by number of ports port_order = sorted( ports, key=lambda port: (ports.get(port, 0), no_sync_ports.get(port, 0)), reverse=True, ) if not port_order: return None, "" most_ports_dpid = port_order[0] most_ports_dpids = [ dpid for dpid, num in ports.items() if num == ports[most_ports_dpid] ] if len(most_ports_dpids) > 1: # There are several dpids that have the same number of lags if root_dpid in most_ports_dpids: # root_dpid is the chosen DPID return root_dpid, "root dp" # Order by lowest DPID return sorted(most_ports_dpids), "lowest dpid" # Most_ports_dpid is the chosen DPID return most_ports_dpid, "most LAG ports"
def _learn_host_intervlan_routing_flows(self, port, vlan, eth_src, eth_dst): """Returns flows for the eth_src_table that enable packets that have been routed to be accepted from an adjacent DP and then switched to the destination. Eth_src_table flow rule to match on port, eth_src, eth_dst and vlan Args: port (Port): Port to match on. vlan (VLAN): VLAN to match on eth_src: source MAC address (should be the router MAC) eth_dst: destination MAC address """ ofmsgs = [] (src_rule_idle_timeout, src_rule_hard_timeout, _) = self._learn_host_timeouts( port, eth_src ) src_match = self.eth_src_table.match( vlan=vlan, eth_src=eth_src, eth_dst=eth_dst ) src_priority = self.host_priority - 1 inst = (self.eth_src_table.goto(self.output_table),) ofmsgs.extend( [ self.eth_src_table.flowmod( match=src_match, priority=src_priority, inst=inst, hard_timeout=src_rule_hard_timeout, idle_timeout=src_rule_idle_timeout, ) ] ) return ofmsgs def _valve_learn_host_from_pkt(self, valve, now, pkt_meta, other_valves): """Add L3 forwarding rule if necessary for inter-VLAN routing.""" ofmsgs_by_valve = super().learn_host_from_pkt( valve, now, pkt_meta, other_valves ) if ( self.stack_manager.stack.route_learning and not self.stack_manager.stack.is_root() ): if pkt_meta.eth_src == pkt_meta.vlan.faucet_mac: ofmsgs_by_valve[valve].extend( self._learn_host_intervlan_routing_flows( pkt_meta.port, pkt_meta.vlan, pkt_meta.eth_src, pkt_meta.eth_dst ) ) elif pkt_meta.eth_dst == pkt_meta.vlan.faucet_mac: ofmsgs_by_valve[valve].extend( self._learn_host_intervlan_routing_flows( pkt_meta.port, pkt_meta.vlan, pkt_meta.eth_dst, pkt_meta.eth_src ) ) return ofmsgs_by_valve
[docs] def learn_host_from_pkt(self, valve, now, pkt_meta, other_valves): ofmsgs_by_valve = {} if self.stack_manager.stack.route_learning: stacked_other_valves = self.stack_manager.stacked_valves(other_valves) all_stacked_valves = {valve}.union(stacked_other_valves) # NOTE: multi DP routing requires learning from directly attached switch first. if pkt_meta.port.stack: peer_dp = pkt_meta.port.stack["dp"] if peer_dp.dyn_running: faucet_macs = {pkt_meta.vlan.faucet_mac}.union( {valve.dp.faucet_dp_mac for valve in all_stacked_valves} ) # Must always learn FAUCET VIP, but rely on neighbor # to learn other hosts first. if pkt_meta.eth_src not in faucet_macs: return ofmsgs_by_valve for other_valve in stacked_other_valves: stack_port = other_valve.stack_manager.relative_port_towards( self.stack_manager.stack.name ) valve_vlan = other_valve.dp.vlans.get(pkt_meta.vlan.vid, None) if stack_port and valve_vlan: valve_pkt_meta = copy.copy(pkt_meta) valve_pkt_meta.vlan = valve_vlan valve_pkt_meta.port = stack_port valve_other_valves = all_stacked_valves - {other_valve} ofmsgs_by_valve.update( self._valve_learn_host_from_pkt( other_valve, now, valve_pkt_meta, valve_other_valves ) ) ofmsgs_by_valve.update( self._valve_learn_host_from_pkt(valve, now, pkt_meta, other_valves) ) return ofmsgs_by_valve
[docs] class ValveSwitchStackManagerNoReflection(ValveSwitchStackManagerBase): """Stacks of size 2 - all switches directly connected to root. Root switch simply floods to all other switches. Non-root switches simply flood to the root. """ def _flood_actions( self, in_port, external_ports, away_flood_actions, toward_flood_actions, local_flood_actions, ): # pylint: disable=too-many-arguments,too-many-positional-arguments if not in_port or self.stack_manager.is_stack_port(in_port): flood_prefix = () else: if external_ports: flood_prefix = self._set_nonext_port_flag else: flood_prefix = self._set_ext_port_flag flood_actions = ( flood_prefix + toward_flood_actions + away_flood_actions + local_flood_actions ) return flood_actions def _edge_dp_for_host(self, other_valves, pkt_meta): """Size 2 means root shortest path is always directly connected.""" peer_dp = pkt_meta.port.stack["dp"] if peer_dp.dyn_running: return self._non_stack_learned(other_valves, pkt_meta) # Fall back to assuming peer knows if we are not the peer's controller. return peer_dp
[docs] class ValveSwitchStackManagerReflection(ValveSwitchStackManagerBase): """Stacks size > 2 reflect floods off of root (selective flooding). .. code-block:: none Hosts |||| |||| +----+ +----+ +----+ ---+1 | |1234| | 1+--- Hosts ---+2 | | | | 2+--- Hosts ---+3 | | | | 3+--- ---+4 5+-------+5 6+-------+5 4+--- +----+ +----+ +----+ Root DP Non-root switches flood only to the root. The root switch reflects incoming floods back out. Non-root switches flood packets from the root locally and to switches further away from the root. Flooding is entirely implemented in the dataplane. A host connected to a non-root switch can receive a copy of its own flooded packet (because the non-root switch does not know it has seen the packet already). A host connected to the root switch does not have this problem (because flooding is always away from the root). Therefore, connections to other non-FAUCET stacking networks should only be made to the root. On the root switch (left), flood destinations are: 1: 2 3 4 5(s) 2: 1 3 4 5(s) 3: 1 2 4 5(s) 4: 1 2 3 5(s) 5: 1 2 3 4 5(s, note reflection) On the middle switch: 1: 5(s) 2: 5(s) 3: 5(s) 4: 5(s) 5: 1 2 3 4 6(s) 6: 5(s) On the rightmost switch: 1: 5(s) 2: 5(s) 3: 5(s) 4: 5(s) 5: 1 2 3 4 """ # Indicate to base class use of reflection required. _USES_REFLECTION = True def _learn_cache_check( self, entry, vlan, now, eth_src, port, ofmsgs, cache_port, cache_age, delete_existing, refresh_rules, ): # pylint: disable=too-many-arguments,too-many-positional-arguments learn_exit = False update_cache = True if cache_port is not None: # packet was received on same member of a LAG. same_lag = port.lacp and port.lacp == cache_port.lacp # stacks of size > 2 will have an unknown MAC flooded towards the root, # and flooded down again. If we learned the MAC on a local port and # heard the reflected flooded copy, discard the reflection. local_stack_learn = port.stack and not cache_port.stack guard_time = self.cache_update_guard_time if cache_port == port or same_lag or local_stack_learn: port_cache_valid = ( port.dyn_update_time is not None and port.dyn_update_time <= entry.cache_time ) # aggressively re-learn on LAGs, and prefer recently learned # locally learned hosts on a stack. if same_lag or local_stack_learn: guard_time = 2 # port didn't change status, and recent cache update, don't do anything. if cache_age < guard_time and port_cache_valid: update_cache = False learn_exit = True # skip delete if host didn't change ports or on same LAG. elif cache_port == port or same_lag: delete_existing = False if port_cache_valid: refresh_rules = True return ( learn_exit, ofmsgs, cache_port, update_cache, delete_existing, refresh_rules, ) def _flood_actions( self, in_port, external_ports, away_flood_actions, toward_flood_actions, local_flood_actions, ): # pylint: disable=too-many-arguments,too-many-positional-arguments if self.stack_manager.stack.is_root(): if external_ports: flood_prefix = self._set_nonext_port_flag else: flood_prefix = self._set_ext_port_flag flood_actions = away_flood_actions + local_flood_actions if in_port and self.stack_manager.is_away(in_port): # Packet from a non-root switch, flood locally and to all non-root switches # (reflect it). flood_actions = ( away_flood_actions + (valve_of.output_in_port(),) + local_flood_actions ) flood_actions = flood_prefix + flood_actions else: # Default non-root strategy is flood towards root. if external_ports: flood_actions = self._set_nonext_port_flag + toward_flood_actions else: flood_actions = self._set_ext_port_flag + toward_flood_actions if in_port: # Packet from switch further away, flood it to the root. if self.stack_manager.is_away(in_port): flood_actions = toward_flood_actions # Packet from the root. elif self.stack_manager.is_towards_root(in_port): # If we have external ports, and packet hasn't already been flooded # externally, flood it externally before passing it to further away switches, # and mark it flooded. if external_ports: flood_actions = ( self._set_nonext_port_flag + away_flood_actions + local_flood_actions ) else: flood_actions = ( away_flood_actions + self._set_nonext_port_flag + local_flood_actions ) # Packet from external port, locally. Mark it already flooded externally and # flood to root (it came from an external switch so keep it within the stack). elif in_port.loop_protect_external: flood_actions = self._set_nonext_port_flag + toward_flood_actions else: flood_actions = self._set_ext_port_flag + toward_flood_actions return flood_actions def _edge_dp_for_host(self, other_valves, pkt_meta): """For stacks size > 2.""" # TODO: currently requires controller to manage all switches # in the stack to keep each DP's graph consistent. # TODO: simplest possible unicast learning. # We find just one port that is the shortest unicast path to # the destination. We could use other factors (eg we could # load balance over multiple ports based on destination MAC). # Find port that forwards closer to destination DP that # has already learned this host (if any). peer_dp = pkt_meta.port.stack["dp"] if peer_dp.dyn_running: return self._non_stack_learned(other_valves, pkt_meta) # Fall back to peer knows if edge or root if we are not the peer's controller. if peer_dp.stack.is_edge() or peer_dp.stack.is_root(): return peer_dp # No DP has learned this host, yet. Take no action to allow remote learning to occur. return None