Source code for faucet.stack

"""Configuration for a stack."""

# 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 Counter
import networkx

from faucet.conf import Conf, test_config_condition


[docs] class Stack(Conf): """Stores state related to DP stack information, this includes the current elected root as that is technically a fixed allocation for this DP Stack instance.""" defaults = { # Sets the root priority value of the current DP with stacking "priority": None, # Use the stack route algorithms, will be forced true if routing is enabled "route_learning": False, # Number of update time intervals for a down stack node to still be considered healthy "down_time_multiple": 3, # Minimum percentage value of required UP stack ports for this stack # node to be considered healthy "min_stack_health": 1.0, # Minimum percentage value of required UP LACP ports for this stack # node to be considered healthy "min_lacp_health": 1.0, } defaults_types = { "priority": int, "route_learning": bool, "down_time_multiple": int, "min_stack_health": float, "min_lacp_health": float, } def __init__( self, _id, dp_id, name, canonical_port_order, lacp_down_ports, lacp_ports, conf ): """ Constructs a new stack object Args: _id (str): Name of the configuration key dp_id (int): DP ID of the DP that holds this stack instance name (str): Name of the DP that holds this stack instance canonical_port_order (func): Function to order ports in a standardized way lacp_down_ports (func): Returns a tuple of the not UP LACP ports for this stack node lacp_ports (func): Returns a tuple of all LACP ports for this stack node conf (dict): Stack configuration """ self.name = name # Function to order ports in a standardized way self.canonical_port_order = canonical_port_order self.lacp_down_ports = lacp_down_ports self.lacp_ports = lacp_ports # Stack configuration options self.priority = None self.route_learning = None self.down_time_multiple = None self.min_stack_health = None self.min_lacp_health = None # Ports that have stacking configured self.ports = [] # Stack graph containing all the DPs & ports in the stacking topology self.graph = None # Additional stacking information self.root_name = None self.roots_names = None self.root_flood_reflection = None # Whether the stack node is currently healthy # dyn_healthy_info := (<running>, <stack ports>, <lacp ports>) self.dyn_healthy_info = (False, 0.0, 0.0) self.dyn_healthy = False super().__init__(_id, dp_id, conf)
[docs] def clone_dyn_state(self, prev_stack, dps=None): """Copy dyn state from the old stack instance when warm/cold starting""" if prev_stack: self.dyn_healthy = prev_stack.dyn_healthy self.dyn_healthy_info = prev_stack.dyn_healthy_info if dps: stack_port_dps = [dp for dp in dps if dp.stack_ports()] for dp in stack_port_dps: for port in dp.stack_ports(): port_up = False if port.is_stack_up(): port_up = True elif port.is_stack_init() and port.stack["port"].is_stack_up(): port_up = True self.modify_link(dp, port, add=port_up)
[docs] def live_timeout_healthy(self, last_live_time, now, update_time): """ Determines the timeout of the current stack node, and whether the current stack node can be considered healthy according to the `down_time_multiple` number of stack root update time intervals. Args: last_live_time (float): Last known live time for this current stack node now (float): Current time update_time (int): Update time interval Return: bool: If node down time is still in update time interval threshold; considered healthy, float: Time elapsed since timed out """ # Time elapsed for the number of safe down time multiples before considered unhealthy down_time = self.down_time_multiple * update_time # Final time at which nodes are still considered healthy health_timeout = now - down_time # If node last known live time was greater than the health timeout then it is healthy timeout_healthy = last_live_time >= health_timeout return timeout_healthy, health_timeout
[docs] def stack_port_healthy(self): """ Determines the percentage of UP stack ports, and whether the current stack node can be considered healthy according to the `min_stack_health` configuration option. Return: bool: Whether threshold from DOWN stack ports is met; considered healthy, float: Percentage of stack ports UP out of all stack ports """ down_ports = self.down_ports() all_ports = self.ports if len(all_ports) == 0: return True, 1.0 percentage = 1.0 - (float(len(down_ports) / float(len(all_ports)))) stack_ports_healthy = percentage >= self.min_stack_health return stack_ports_healthy, percentage
[docs] def lacp_port_healthy(self): """ Determines the percentage of UP LACP ports, and whether the current stack node can be considered healthy according to the `min_lacp_health` configuration option. Return: bool: Whether threshold from DOWN LACP ports is met; considered healthy, float: Percentage of LACP ports UP out of all lacp ports """ down_ports = self.lacp_down_ports() all_ports = self.lacp_ports() if len(all_ports) == 0: return True, 1.0 percentage = 1.0 - (float(len(down_ports) / float(len(all_ports)))) lacp_ports_healthy = percentage >= self.min_lacp_health return lacp_ports_healthy, percentage
[docs] def update_health(self, now, dp_last_live_time, update_time): """ Determines whether the current stack node is healthy Args: now (float): Current time last_live_times (dict): Last live time value for each DP update_time (int): Stack root update interval time Return: tuple: Current stack node health state, str: Reason for the current state """ reason = "" last_live_time = dp_last_live_time.get(self.name, 0) timeout_healthy, health_timeout = self.live_timeout_healthy( last_live_time, now, update_time ) if not timeout_healthy: # Too long since DP last running, if DP not running then # number of UP stack or LACP ports should be 0 reason += "last running %us ago (timeout %us)" % ( now - last_live_time, health_timeout, ) self.dyn_healthy_info = (False, 0.0, 0.0) self.dyn_healthy = False return self.dyn_healthy, reason reason += "running %us ago" % (now - last_live_time) if reason: reason += ", " stack_ports_healthy, stack_percentage = self.stack_port_healthy() if not stack_ports_healthy: # The number of DOWN stack ports surpasses the threshold for DOWN stack port tolerance reason += "stack ports %s (%.0f%%) not up" % ( list(self.down_ports()), (1.0 - stack_percentage) * 100.0, ) else: reason += "%.0f%% stack ports running" % (stack_percentage * 100.0) if self.lacp_ports(): if reason: reason += ", " lacp_ports_healthy, lacp_percentage = self.lacp_port_healthy() if not lacp_ports_healthy: # The number of DOWN LACP ports surpasses the threshold for DOWN LACP port tolerance reason += "lacp ports %s (%.0f%%) not up" % ( list(self.lacp_down_ports()), (1.0 - lacp_percentage) * 100.0, ) else: reason += "%.0f%% lacp ports running" % (lacp_percentage * 100.0) else: # No LACP ports in node, so default to 100% UP & don't report information lacp_ports_healthy = True lacp_percentage = 0.0 self.dyn_healthy_info = (timeout_healthy, stack_percentage, lacp_percentage) if timeout_healthy and stack_ports_healthy and lacp_ports_healthy: self.dyn_healthy = True else: self.dyn_healthy = False return self.dyn_healthy, reason
[docs] @staticmethod def nominate_stack_root(stacks): """Return stack names in priority order and the chosen root""" def health_priority(stack): # Invert the health priority info so it is sorted correctly # in relation to priority and the binary health invert_info = ( 1.0 - stack.dyn_healthy_info[1], 1.0 - stack.dyn_healthy_info[2], ) return (not stack.dyn_healthy, *invert_info, stack.priority, stack.dp_id) stack_priorities = sorted(stacks, key=health_priority) priority_names = tuple(stack.name for stack in stack_priorities) nominated_name = priority_names[0] return priority_names, nominated_name
[docs] def resolve_topology(self, dps, meta_dp_state): """ Resolve & verify correct inter-DP stacking config Args: dps (list): List of configured DPs meta_dp_state (MetaDPState): Provided if reloading when choosing a new root DP """ stack_dps = [dp for dp in dps if dp.stack is not None] stack_priority_dps = [dp for dp in stack_dps if dp.stack.priority] stack_port_dps = [dp for dp in dps if dp.stack_ports()] if not stack_priority_dps: test_config_condition(stack_dps, "stacking enabled but no root DP") return if not self.ports: return for dp in stack_priority_dps: test_config_condition( not isinstance(dp.stack.priority, int), ( "stack priority must be type %s not %s" % (int, type(dp.stack.priority)) ), ) test_config_condition( dp.stack.priority <= 0, ("stack priority must be > 0") ) self.roots_names, self.root_name = self.nominate_stack_root( [dp.stack for dp in stack_priority_dps] ) if meta_dp_state: # If meta_dp_state exists, then we are reloading a new instance of the stack # for a new 'dynamically' chosen root if meta_dp_state.stack_root_name in self.roots_names: self.root_name = meta_dp_state.stack_root_name for dp in stack_port_dps: for vlan in dp.vlans.values(): if vlan.faucet_vips: self.route_learning = True edge_count = Counter() graph = networkx.MultiGraph() for dp in stack_port_dps: graph.add_node(dp.name) for port in dp.stack_ports(): edge_name = Stack.modify_topology(graph, dp, port) edge_count[edge_name] += 1 for edge_name, count in edge_count.items(): test_config_condition( count != 2, "%s defined only in one direction" % edge_name ) if graph.size() and self.name in graph: self.graph = graph for dp in graph.nodes(): path_to_root_len = len(self.shortest_path(self.root_name, src_dp=dp)) test_config_condition( path_to_root_len == 0, "%s not connected to stack" % dp ) root_len = self.longest_path_to_root_len() if root_len is not None and root_len > 2: self.root_flood_reflection = True
[docs] @staticmethod def modify_topology(graph, dp, port, add=True): """Add/remove an edge to the stack graph which originates from this dp and port.""" def canonical_edge(dp, port): peer_dp = port.stack["dp"] peer_port = port.stack["port"] sort_edge_a = (dp.name, port.name, dp, port) sort_edge_z = (peer_dp.name, peer_port.name, peer_dp, peer_port) sorted_edge = sorted((sort_edge_a, sort_edge_z)) edge_a, edge_b = sorted_edge[0][2:], sorted_edge[1][2:] return edge_a, edge_b def make_edge_name(edge_a, edge_z): edge_a_dp, edge_a_port = edge_a edge_z_dp, edge_z_port = edge_z return "%s:%s-%s:%s" % ( edge_a_dp.name, edge_a_port.name, edge_z_dp.name, edge_z_port.name, ) def make_edge_attr(edge_a, edge_z): edge_a_dp, edge_a_port = edge_a edge_z_dp, edge_z_port = edge_z return { "dp_a": edge_a_dp, "port_a": edge_a_port, "dp_z": edge_z_dp, "port_z": edge_z_port, } edge = canonical_edge(dp, port) edge_a, edge_z = edge edge_name = make_edge_name(edge_a, edge_z) edge_attr = make_edge_attr(edge_a, edge_z) edge_a_dp, _ = edge_a edge_z_dp, _ = edge_z if add: graph.add_edge( edge_a_dp.name, edge_z_dp.name, key=edge_name, port_map=edge_attr ) elif (edge_a_dp.name, edge_z_dp.name, edge_name) in graph.edges: graph.remove_edge(edge_a_dp.name, edge_z_dp.name, edge_name) return edge_name
[docs] def hash(self): """Return hash of a topology graph""" return hash(tuple(sorted(self.graph.degree())))
[docs] def add_port(self, port): """Add a port to this stack""" self.ports.append(port)
[docs] def any_port_up(self): """Return true if any stack port is UP""" for port in self.ports: if port.is_stack_up(): return True return False
[docs] def down_ports(self): """Return tuple of not running stack ports""" return tuple(port for port in self.ports if not port.is_stack_up())
[docs] def canonical_up_ports(self, ports=None): """Obtains list of UP stack ports in canonical order""" if ports is None: ports = self.ports return self.canonical_port_order([port for port in ports if port.is_stack_up()])
[docs] def shortest_path(self, dest_dp, src_dp=None): """Return shortest path to a DP, as a list of DPs.""" if src_dp is None: src_dp = self.name if self.graph: try: return sorted(networkx.all_shortest_paths(self.graph, src_dp, dest_dp))[ 0 ] except (networkx.exception.NetworkXNoPath, networkx.exception.NodeNotFound): pass return []
[docs] def shortest_path_to_root(self, src_dp=None): """Return shortest path to root DP, as list of DPs.""" return self.shortest_path(self.root_name, src_dp=src_dp)
[docs] def is_root(self): """Return True if this DP is the root of the stack.""" return self.name == self.root_name
[docs] def is_root_candidate(self): """Return True if this DP could be a root of the stack.""" return self.name in self.roots_names
[docs] def is_edge(self): """Return True if this DP is a stack edge.""" return not self.is_root() and self.longest_path_to_root_len() == len( self.shortest_path_to_root() )
[docs] def shortest_path_port(self, dest_dp): """Return first port on our DP, that is the shortest path towards dest DP.""" shortest_path = self.shortest_path(dest_dp) if len(shortest_path) > 1: peer_dp = shortest_path[1] peer_dp_ports = self.peer_up_ports(peer_dp) if peer_dp_ports: return peer_dp_ports[0] return None
[docs] def peer_up_ports(self, peer_dp): """Return list of stack ports that are up towards a peer.""" return self.canonical_port_order( [ port for port in self.ports if port.running() and (port.stack["dp"].name == peer_dp) ] )
[docs] def longest_path_to_root_len(self): """Return length of the longest path to root in the stack.""" if not self.graph or not self.root_name: return None len_paths_to_root = [ len(self.shortest_path(self.root_name, src_dp=dp)) for dp in self.graph.nodes() ] if len_paths_to_root: return max(len_paths_to_root) return None
[docs] def is_in_path(self, src_dp, dst_dp): """Return True if the current DP is in the path from src_dp to dst_dp Args: src_dp (str): DP name dst_dp (str): DP name Returns: bool: True if self is in the path from the src_dp to the dst_dp. """ path = self.shortest_path(dst_dp, src_dp=src_dp) return self.name in path
[docs] def peer_symmetric_up_ports(self, peer_dp): """Return list of stack ports that are up towards us from a peer""" # Sort adjacent ports by canonical port order return self.canonical_port_order( [ port.stack["port"] for port in self.ports if port.running() and (port.stack["dp"].name == peer_dp) ] )
[docs] def shortest_symmetric_path_port(self, peer_dp): """Return port on our DP that is the first port of the adjacent DP towards us""" shortest_path = self.shortest_path(self.name, src_dp=peer_dp) if len(shortest_path) == 2: adjacent_up_ports = self.peer_symmetric_up_ports(peer_dp) if adjacent_up_ports: return adjacent_up_ports[0].stack["port"] return None