"""Manage flooding to ports on VLANs."""
# 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.
from collections import defaultdict
from faucet import valve_of
from faucet import valve_packet
from faucet.valve_manager_base import ValveManagerBase
[docs]class ValveFloodManager(ValveManagerBase):
"""Implement dataplane based flooding for standalone dataplanes."""
# Enumerate possible eth_dst flood destinations.
# First bool says whether to flood this destination if the VLAN
# has unicast flooding enabled (if unicast flooding is enabled,
# then we flood all destination eth_dsts).
FLOOD_DSTS = (
(True, None, None, None),
(False, None, valve_packet.BRIDGE_GROUP_ADDRESS, valve_packet.mac_byte_mask(3)), # 802.x
(False, None, '01:00:5E:00:00:00', valve_packet.mac_byte_mask(3)), # IPv4 multicast
(False, None, '33:33:00:00:00:00', valve_packet.mac_byte_mask(2)), # IPv6 multicast
(False, None, valve_of.mac.BROADCAST_STR, valve_packet.mac_byte_mask(6)), # eth broadcasts
)
# Ports with restricted broadcast enabled may only receive these broadcasts.
RESTRICTED_FLOOD_DISTS = (
(False, valve_of.ether.ETH_TYPE_ARP,
valve_of.mac.BROADCAST_STR, valve_packet.mac_byte_mask(6)), # ARP
(False, valve_of.ether.ETH_TYPE_IPV6,
'33:33:FF:00:00:00', valve_packet.mac_byte_mask(3)), # IPv6 multicast for ND
(False, valve_of.ether.ETH_TYPE_IPV6,
valve_packet.IPV6_ALL_ROUTERS_MCAST, valve_packet.mac_byte_mask(6)), # IPV6 all routers
(False, valve_of.ether.ETH_TYPE_IPV6,
valve_packet.IPV6_ALL_NODES_MCAST, valve_packet.mac_byte_mask(6)), # IPv6 all nodes
)
def __init__(self, logger, flood_table, pipeline, # pylint: disable=too-many-arguments
use_group_table, groups, combinatorial_port_flood,
canonical_port_order, restricted_bcast_arpnd):
self.logger = logger
self.flood_table = flood_table
self.pipeline = pipeline
self.use_group_table = use_group_table
self.groups = groups
self.combinatorial_port_flood = combinatorial_port_flood
self.bypass_priority = self._FILTER_PRIORITY
self.flood_priority = self._MATCH_PRIORITY
self.classification_offset = 0x100
self.canonical_port_order = canonical_port_order
self.restricted_bcast_arpnd = restricted_bcast_arpnd
if restricted_bcast_arpnd:
self.flood_dsts = self.FLOOD_DSTS + self.RESTRICTED_FLOOD_DISTS
else:
self.flood_dsts = self.FLOOD_DSTS
[docs] def initialise_tables(self):
"""Initialise the flood table with filtering flows."""
ofmsgs = []
for eth_dst, eth_dst_mask in (
(valve_packet.CISCO_CDP_VTP_UDLD_ADDRESS, valve_packet.mac_byte_mask(6)),
(valve_packet.CISCO_SPANNING_GROUP_ADDRESS, valve_packet.mac_byte_mask(6)),
(valve_packet.BRIDGE_GROUP_ADDRESS, valve_packet.BRIDGE_GROUP_MASK)):
ofmsgs.append(self.flood_table.flowdrop(
self.flood_table.match(eth_dst=eth_dst, eth_dst_mask=eth_dst_mask),
priority=self._mask_flood_priority(eth_dst_mask)))
return ofmsgs
[docs] def floods_to_root(self, _dp_obj):
"""Return True if the given dp floods (only) to root switch"""
return False
def _mask_flood_priority(self, eth_dst_mask):
return self.flood_priority + valve_packet.mac_mask_bits(eth_dst_mask)
@staticmethod
def _vlan_all_ports(vlan, exclude_unicast):
"""Return list of all ports that should be flooded to on a VLAN."""
return list(vlan.flood_ports(vlan.get_ports(), exclude_unicast))
def _build_flood_local_rule_actions(self, vlan, exclude_unicast, in_port, # pylint: disable=too-many-arguments
exclude_all_external, exclude_restricted_bcast_arpnd):
"""Return a list of flood actions to flood packets from a port."""
external_ports = self.canonical_port_order(vlan.loop_protect_external_ports_up())
exclude_ports = vlan.exclude_same_lag_member_ports(in_port)
exclude_ports.update(vlan.exclude_native_if_dot1x())
if exclude_all_external or (in_port is not None and in_port.loop_protect_external):
exclude_ports.update(set(external_ports))
else:
exclude_ports.update(set(external_ports[1:]))
if exclude_restricted_bcast_arpnd:
exclude_ports.update(set(vlan.restricted_bcast_arpnd_ports()))
return valve_of.flood_port_outputs(
vlan.tagged_flood_ports(exclude_unicast),
vlan.untagged_flood_ports(exclude_unicast),
in_port=in_port,
exclude_ports=exclude_ports)
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):
actions = []
if vlan.loop_protect_external_ports() and vlan.tagged_flood_ports(exclude_unicast):
actions.append(self.flood_table.set_external_forwarding_requested())
actions.extend(self._build_flood_local_rule_actions(
vlan, exclude_unicast, in_port, exclude_all_external, exclude_restricted_bcast_arpnd))
return tuple(actions)
def _build_flood_rule(self, match, command, flood_acts, flood_priority):
return self.flood_table.flowmod(
match=match,
command=command,
inst=[valve_of.apply_actions(flood_acts)],
priority=flood_priority)
def _vlan_flood_priority(self, eth_type, eth_dst_mask):
priority = self._mask_flood_priority(eth_dst_mask)
if eth_type:
priority += eth_type
return priority
def _build_flood_rule_for_vlan(self, vlan, eth_type, eth_dst, eth_dst_mask, # pylint: disable=too-many-arguments
exclude_unicast, command):
flood_priority = self._vlan_flood_priority(eth_type, eth_dst_mask)
match = self.flood_table.match(
vlan=vlan, eth_type=eth_type, eth_dst=eth_dst, eth_dst_mask=eth_dst_mask)
# TODO: optimization - drop all general flood dsts if all ports are restricted.
exclude_restricted_bcast_arpnd = True
flood_acts = self._build_flood_rule_actions(
vlan, exclude_unicast, None,
exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd)
return (self._build_flood_rule(match, command, flood_acts, flood_priority), flood_acts)
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):
flood_acts = ()
port_output_ports = []
port_non_output_acts = []
if port.dyn_phys_up:
if exclude_restricted_bcast_arpnd:
flood_acts = self._build_flood_rule_actions(
vlan, exclude_unicast, port, exclude_all_external, port.restricted_bcast_arpnd)
else:
flood_acts = self._build_flood_rule_actions(
vlan, exclude_unicast, port, exclude_all_external, False)
(flood_acts,
port_output_ports,
port_non_output_acts) = valve_of.output_non_output_actions(flood_acts)
if not port_output_ports:
flood_acts = ()
port_non_output_acts = ()
return (flood_acts, port_output_ports, port_non_output_acts)
def _build_flood_match_priority(self, port, vlan, eth_type, # pylint: disable=too-many-arguments
eth_dst, eth_dst_mask, add_match):
flood_priority = self._vlan_flood_priority(eth_type, eth_dst_mask) + 1
if add_match is None:
add_match = {}
match = self.flood_table.match(
vlan=vlan, in_port=port.number,
eth_type=eth_type, eth_dst=eth_dst, eth_dst_mask=eth_dst_mask,
**add_match)
return (flood_priority, match)
def _build_flood_rule_for_port(self, vlan, eth_type, eth_dst, eth_dst_mask, # pylint: disable=too-many-arguments
command, port, flood_acts, add_match=None):
flood_priority, match = self._build_flood_match_priority(
port, vlan, eth_type, eth_dst, eth_dst_mask, add_match)
return self._build_flood_rule(match, command, flood_acts, flood_priority)
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):
ofmsgs = []
if self.combinatorial_port_flood:
for port in self._vlan_all_ports(vlan, exclude_unicast):
flood_acts, _, _ = self._build_flood_acts_for_port(
vlan, exclude_unicast, port,
exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd)
if flood_acts:
ofmsgs.append(self._build_flood_rule_for_port(
vlan, eth_type, eth_dst, eth_dst_mask, command, port, flood_acts))
else:
vlan_flood_ofmsg, vlan_flood_acts = self._build_flood_rule_for_vlan(
vlan, eth_type, eth_dst, eth_dst_mask, exclude_unicast, command)
if not self.use_group_table:
ofmsgs.append(vlan_flood_ofmsg)
(flood_acts,
vlan_output_ports,
vlan_non_output_acts) = valve_of.output_non_output_actions(vlan_flood_acts)
for port in self._vlan_all_ports(vlan, exclude_unicast):
(flood_acts,
port_output_ports,
port_non_output_acts) = self._build_flood_acts_for_port(
vlan, exclude_unicast, port,
exclude_restricted_bcast_arpnd=exclude_restricted_bcast_arpnd)
if not flood_acts:
continue
if (vlan_output_ports - set([port.number]) == port_output_ports
and vlan_non_output_acts == port_non_output_acts):
# Delete a potentially existing port specific flow
# TODO: optimize, avoid generating delete for port if no existing flow.
flood_priority, match = self._build_flood_match_priority(
port, vlan, eth_type, eth_dst, eth_dst_mask, add_match=None)
ofmsgs.append(self.flood_table.flowdel(
match=match, priority=flood_priority))
else:
ofmsgs.append(self._build_flood_rule_for_port(
vlan, eth_type, eth_dst, eth_dst_mask, command, port, flood_acts))
return ofmsgs
def _build_multiout_flood_rules(self, vlan, command):
"""Build flooding rules for a VLAN without using groups."""
ofmsgs = []
for unicast_eth_dst, eth_type, eth_dst, eth_dst_mask in self.flood_dsts:
if unicast_eth_dst and not vlan.unicast_flood:
continue
exclude_restricted_bcast_arpnd = eth_type is None
ofmsgs.extend(self._build_mask_flood_rules(
vlan, eth_type, eth_dst, eth_dst_mask,
unicast_eth_dst, exclude_restricted_bcast_arpnd, command))
return ofmsgs
def _build_group_flood_rules(self, vlan, modify, command):
"""Build flooding rules for a VLAN using groups."""
_, vlan_flood_acts = self._build_flood_rule_for_vlan(
vlan, None, None, None, False, command)
group_id = vlan.vid
group = self.groups.get_entry(
group_id, valve_of.build_group_flood_buckets(vlan_flood_acts))
groups_by_unicast_eth = {False: group, True: group}
ofmsgs = []
# Only configure unicast flooding group if has different output
# actions to non unicast flooding.
_, unicast_eth_vlan_flood_acts = self._build_flood_rule_for_vlan(
vlan, None, None, None, True, command)
unicast_eth_vlan_flood_acts, unicast_output_ports, _ = valve_of.output_non_output_actions(
unicast_eth_vlan_flood_acts)
vlan_flood_acts, vlan_output_ports, _ = valve_of.output_non_output_actions(vlan_flood_acts)
if unicast_output_ports != vlan_output_ports:
group_id += valve_of.VLAN_GROUP_OFFSET
group = self.groups.get_entry(
group_id, valve_of.build_group_flood_buckets(unicast_eth_vlan_flood_acts))
groups_by_unicast_eth[True] = group
for group in groups_by_unicast_eth.values():
if modify:
ofmsgs.append(group.modify())
else:
ofmsgs.extend(group.add())
for unicast_eth_dst, eth_type, eth_dst, eth_dst_mask in self.flood_dsts:
if unicast_eth_dst and not vlan.unicast_flood:
continue
group = groups_by_unicast_eth[unicast_eth_dst]
match = self.flood_table.match(
vlan=vlan, eth_type=eth_type, eth_dst=eth_dst, eth_dst_mask=eth_dst_mask)
flood_priority = self._vlan_flood_priority(eth_type, eth_dst_mask)
ofmsgs.append(self.flood_table.flowmod(
match=match,
command=command,
inst=[valve_of.apply_actions([valve_of.group_act(group.group_id)])],
priority=flood_priority))
return ofmsgs
[docs] def add_vlan(self, vlan):
return self.build_flood_rules(vlan)
[docs] def del_vlan(self, vlan):
return [self.flood_table.flowdel(self.flood_table.match(vlan=vlan.vid))]
[docs] def update_vlan(self, vlan):
return self.build_flood_rules(vlan, modify=True)
[docs] def build_flood_rules(self, vlan, modify=False):
"""Add flows to flood packets to unknown destinations on a VLAN."""
command = valve_of.ofp.OFPFC_ADD
if modify:
command = valve_of.ofp.OFPFC_MODIFY_STRICT
ofmsgs = self._build_multiout_flood_rules(vlan, command)
if self.use_group_table:
ofmsgs.extend(self._build_group_flood_rules(vlan, modify, command))
return ofmsgs
[docs] @staticmethod
def update_stack_topo(event, dp, port): # pylint: disable=unused-argument,invalid-name
"""Update the stack topology. It has nothing to do for non-stacking DPs."""
return
[docs] @staticmethod
def edge_learn_port(_other_valves, pkt_meta):
"""Possibly learn a host on a port.
Args:
other_valves (list): All Valves other than this one.
pkt_meta (PacketMeta): PacketMeta instance for packet received.
Returns:
port to learn host on.
"""
return pkt_meta.port
[docs]class ValveFloodStackManagerBase(ValveFloodManager):
"""Base class for dataplane based flooding on stacked dataplanes."""
# By default, no reflection used for flooding algorithms.
_USES_REFLECTION = False
def __init__(self, logger, flood_table, pipeline, # pylint: disable=too-many-arguments
use_group_table, groups,
combinatorial_port_flood, canonical_port_order,
restricted_bcast_arpnd,
stack_ports, has_externals,
dp_shortest_path_to_root, shortest_path_port,
is_stack_root, is_stack_root_candidate,
is_stack_edge, graph):
super(ValveFloodStackManagerBase, self).__init__(
logger, flood_table, pipeline,
use_group_table, groups,
combinatorial_port_flood,
canonical_port_order,
restricted_bcast_arpnd)
self.stack_ports = stack_ports
self.canonical_port_order = canonical_port_order
self.externals = has_externals
self.shortest_path_port = shortest_path_port
self.dp_shortest_path_to_root = dp_shortest_path_to_root
self.is_stack_root = is_stack_root
self.is_stack_root_candidate = is_stack_root_candidate
self.is_stack_edge = is_stack_edge
self.graph = graph
self._set_ext_port_flag = ()
self._set_nonext_port_flag = ()
self.external_root_only = False
if self.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.is_stack_root() and self.is_stack_root_candidate():
self.logger.info('external flooding on root only')
self.external_root_only = True
self._reset_peer_distances()
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(ValveFloodStackManagerBase, self)._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):
raise NotImplementedError
def _reset_peer_distances(self):
"""Reset distances to/from root for this DP."""
self.all_towards_root_stack_ports = set()
self.towards_root_stack_ports = set()
self.away_from_root_stack_ports = set()
all_peer_ports = set(self._canonical_stack_up_ports(self.stack_ports))
if self.is_stack_root():
self.away_from_root_stack_ports = all_peer_ports
else:
port_peer_distances = {
port: len(port.stack['dp'].shortest_path_to_root()) for port in all_peer_ports}
shortest_peer_distance = None
for port, port_peer_distance in port_peer_distances.items():
if shortest_peer_distance is None:
shortest_peer_distance = port_peer_distance
continue
shortest_peer_distance = min(shortest_peer_distance, port_peer_distance)
self.all_towards_root_stack_ports = {
port for port, port_peer_distance in port_peer_distances.items()
if port_peer_distance == shortest_peer_distance}
if self.all_towards_root_stack_ports:
# Choose the port that is the chosen shortest path towards the root
shortest_path = self.dp_shortest_path_to_root()
if shortest_path and len(shortest_path) > 1:
first_peer_dp = self.dp_shortest_path_to_root()[1]
else:
first_peer_port = self.canonical_port_order(
self.all_towards_root_stack_ports)[0]
first_peer_dp = first_peer_port.stack['dp'].name
self.towards_root_stack_ports = {
port for port in self.all_towards_root_stack_ports
if port.stack['dp'].name == first_peer_dp}
self.away_from_root_stack_ports = all_peer_ports - self.all_towards_root_stack_ports
if self.towards_root_stack_ports:
self.logger.info(
'shortest path to root is via %s' % self.towards_root_stack_ports)
else:
self.logger.info('no path available to root')
def _build_flood_rule_actions(self, vlan, exclude_unicast, in_port,
exclude_all_external=False, exclude_restricted_bcast_arpnd=False):
exclude_ports = self._inactive_away_stack_ports()
external_ports = vlan.loop_protect_external_ports()
if in_port and in_port in self.stack_ports:
in_port_peer_dp = in_port.stack['dp']
exclude_ports = exclude_ports + [
port for port in self.stack_ports
if port.stack['dp'] == 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.away_from_root_stack_ports, in_port, exclude_ports=exclude_ports))
toward_flood_actions = tuple(valve_of.flood_tagged_port_outputs(
self.towards_root_stack_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 _inactive_away_stack_ports(self):
all_peer_ports = set(self._canonical_stack_up_ports(self.stack_ports))
shortest_path = self.dp_shortest_path_to_root()
if not shortest_path or len(shortest_path) < 2:
return []
self_dp = shortest_path[0]
inactive = []
for port in all_peer_ports:
shortest_path = port.stack['dp'].shortest_path_to_root()
if len(shortest_path) > 1 and shortest_path[1] != self_dp:
inactive.append(port)
return inactive
def _canonical_stack_up_ports(self, ports):
return self.canonical_port_order([port for port in ports if port.is_stack_up()])
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):
# Stack ports aren't in VLANs, so need special rules to cause flooding from them.
ofmsgs = super(ValveFloodStackManagerBase, self)._build_mask_flood_rules(
vlan, eth_type, eth_dst, eth_dst_mask,
exclude_unicast, exclude_restricted_bcast_arpnd, command)
away_up_ports_by_dp = defaultdict(list)
for port in self._canonical_stack_up_ports(self.away_from_root_stack_ports):
away_up_ports_by_dp[port.stack['dp']].append(port)
towards_up_port = None
towards_up_ports = self._canonical_stack_up_ports(self.towards_root_stack_ports)
if towards_up_ports:
towards_up_port = towards_up_ports[0]
replace_priority_offset = (
self.classification_offset - (
self.pipeline.filter_priority - self.pipeline.select_priority))
for port in self.stack_ports:
remote_dp = port.stack['dp']
away_up_port = None
away_up_ports = away_up_ports_by_dp.get(remote_dp, None)
if away_up_ports:
# Pick the lowest port number on the remote DP.
remote_away_ports = self.canonical_port_order(
[away_port.stack['port'] for away_port in away_up_ports])
away_up_port = remote_away_ports[0].stack['port']
away_port = port in self.away_from_root_stack_ports
towards_port = not away_port
flood_acts = []
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})
# Prune broadcast flooding where multiply connected to same DP
if towards_port:
prune = port != towards_up_port
else:
prune = port != away_up_port
else:
# Do not prune unicast, may be reply from directly connected DP.
prune = False
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.is_stack_root() and eth_dst is not None and self._USES_REFLECTION:
# If ths 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.is_stack_edge() or towards_port:
ofmsgs.extend(self.pipeline.select_packets(
self.flood_table, match,
priority_offset=self.classification_offset))
if self.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
[docs] def update_stack_topo(self, event, dp, port):
"""Update the stack topo according to the event."""
if self.graph is None:
return
if event:
dp.add_stack_link(self.graph, dp, port)
else:
dp.remove_stack_link(self.graph, dp, port)
self._reset_peer_distances()
[docs] def shortest_path_root(self, edge_dp_name):
"""Return the port along the shortest path to/from root for edge learning"""
path_to_root = self.dp_shortest_path_to_root()
if not path_to_root:
return self.shortest_path_port(edge_dp_name)
this_dp = path_to_root[0]
path_from_edge = self.dp_shortest_path_to_root(edge_dp_name)
# If this is the edge switch, then learn using default algorithm.
if not path_from_edge or this_dp == path_from_edge[0]:
return self.shortest_path_port(edge_dp_name)
# If this switch is along the path towards the edge, then head away.
if this_dp in path_from_edge:
away_dp = path_from_edge[path_from_edge.index(this_dp) - 1]
all_away_up_ports = self._canonical_stack_up_ports(self.away_from_root_stack_ports)
away_up_ports = [port for port in all_away_up_ports if port.stack['dp'].name == away_dp]
return away_up_ports[0] if away_up_ports else None
# If not, then head towards the root.
towards_up_ports = self._canonical_stack_up_ports(self.towards_root_stack_ports)
return towards_up_ports[0] if towards_up_ports else None
def _edge_learn_port_towards(self, pkt_meta, edge_dp):
if pkt_meta.vlan.edge_learn_stack_root:
return self.shortest_path_root(edge_dp.name)
else:
return self.shortest_path_port(edge_dp.name)
[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:
edge_dp = self._edge_dp_for_host(other_valves, pkt_meta)
if edge_dp:
return self._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._edge_learn_port_towards(pkt_meta, edge_dp)
# Locally learn.
return super(ValveFloodStackManagerBase, self).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
def _non_stack_learned(self, other_valves, pkt_meta):
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
[docs]class ValveFloodStackManagerNoReflection(ValveFloodStackManagerBase):
"""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):
if not in_port or in_port in self.stack_ports:
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 ValveFloodStackManagerReflection(ValveFloodStackManagerBase):
"""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 _flood_actions(self, in_port, external_ports,
away_flood_actions, toward_flood_actions, local_flood_actions):
if self.is_stack_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 in_port in self.away_from_root_stack_ports:
# 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 in_port in self.away_from_root_stack_ports:
flood_actions = toward_flood_actions
# Packet from the root.
elif in_port in self.all_towards_root_stack_ports:
# 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).
# TODO: edge DPs could use a different forwarding algorithm
# (for example, just default switch to a neighbor).
# 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.is_stack_edge() or peer_dp.is_stack_root():
return peer_dp
# No DP has learned this host, yet. Take no action to allow remote learning to occur.
return None