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, # pylint: disable=too-many-arguments exclude_all_external=False, exclude_restricted_bcast_arpnd=False): 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, # pylint: disable=too-many-arguments away_flood_actions, toward_flood_actions, local_flood_actions): raise NotImplementedError def _build_flood_rule_actions(self, vlan, exclude_unicast, in_port, # pylint: disable=too-many-arguments exclude_all_external=False, exclude_restricted_bcast_arpnd=False): """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): """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 """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, # pylint: disable=too-many-arguments exclude_unicast, exclude_restricted_bcast_arpnd, command, cold_start): """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, # pylint: disable=too-many-arguments away_flood_actions, toward_flood_actions, local_flood_actions): 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): 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, # pylint: disable=too-many-arguments away_flood_actions, toward_flood_actions, local_flood_actions): 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