"""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 modify_link(self, dp, port, add=True):
"""Update the stack topology according to the event"""
return Stack.modify_topology(self.graph, dp, port, add)
[docs] def hash(self):
"""Return hash of a topology graph"""
return hash(tuple(sorted(self.graph.degree())))
[docs] def get_node_link_data(self):
"""Return network stacking graph as a node link representation"""
return networkx.readwrite.json_graph.node_link_data(self.graph)
[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