aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Bailey <anarkiwi@users.noreply.github.com>2020-05-22 09:49:08 +1200
committerGitHub <noreply@github.com>2020-05-22 09:49:08 +1200
commit6cbb088e0fe281fccfb79f47d89dba70294f4d2b (patch)
tree1a3d8c4981f0c2eefcd5de49352008dac998d39f
parente710f58b02f4fa4833748eb6957e1fa902e6bb30 (diff)
parentaa53a2e1354057fed45c64ab43cb985917058f23 (diff)
downloadfaucet-6cbb088e0fe281fccfb79f47d89dba70294f4d2b.tar.gz
faucet-6cbb088e0fe281fccfb79f47d89dba70294f4d2b.tar.bz2
faucet-6cbb088e0fe281fccfb79f47d89dba70294f4d2b.zip
Merge pull request #3565 from mab68/stackwarmstart2
Don't take stack ports down when warm starting
-rw-r--r--clib/config_generator.py2
-rw-r--r--clib/mininet_test_base.py16
-rw-r--r--clib/mininet_test_base_topo.py69
-rw-r--r--faucet/conf.py2
-rw-r--r--faucet/dp.py23
-rw-r--r--faucet/port.py12
-rw-r--r--faucet/valve.py2
-rw-r--r--tests/integration/mininet_multidp_tests.py234
-rwxr-xr-xtests/unit/faucet/test_valve_stack.py213
-rw-r--r--tests/unit/faucet/valve_test_lib.py5
10 files changed, 522 insertions, 56 deletions
diff --git a/clib/config_generator.py b/clib/config_generator.py
index cb68095d..f3365b9c 100644
--- a/clib/config_generator.py
+++ b/clib/config_generator.py
@@ -512,7 +512,7 @@ class FaucetTopoGenerator(Topo):
if include:
config['include'] = list(include)
if include_optional:
- config['include_optional'] = list(include_optional)
+ config['include-optional'] = list(include_optional)
if acl_options:
config['acls'] = self.get_acls_config(acl_options)
config['vlans'] = self.get_vlans_config(n_vlans, vlan_options)
diff --git a/clib/mininet_test_base.py b/clib/mininet_test_base.py
index 6bb42b08..48090aeb 100644
--- a/clib/mininet_test_base.py
+++ b/clib/mininet_test_base.py
@@ -1470,7 +1470,7 @@ dbs:
self.gauge_controller, int(self.gauge_of_port), 1))
def reload_conf(self, yaml_conf, conf_path, restart, cold_start,
- change_expected=True, host_cache=None, hup=True):
+ change_expected=True, host_cache=None, hup=True, dpid=True):
def _update_conf(conf_path, yaml_conf):
if yaml_conf:
@@ -1482,16 +1482,16 @@ dbs:
self.verify_faucet_reconf,
cold_start=cold_start,
change_expected=change_expected,
- reconf_funcs=[update_conf_func], hup=hup)
+ reconf_funcs=[update_conf_func], hup=hup, dpid=dpid)
if restart:
if host_cache:
vlan_labels = dict(vlan=host_cache)
old_mac_table = sorted(self.scrape_prometheus_var(
- 'learned_macs', labels=vlan_labels, multiple=True, default=[]))
+ 'learned_macs', labels=vlan_labels, multiple=True, default=[], dpid=dpid))
verify_faucet_reconf_func()
new_mac_table = sorted(self.scrape_prometheus_var(
- 'learned_macs', labels=vlan_labels, multiple=True, default=[]))
+ 'learned_macs', labels=vlan_labels, multiple=True, default=[], dpid=dpid))
self.assertFalse(
cold_start, msg='host cache is not maintained with cold start')
self.assertTrue(
@@ -2049,13 +2049,13 @@ dbs:
def verify_faucet_reconf(self, timeout=10,
cold_start=True, change_expected=True,
- hup=True, reconf_funcs=None):
+ hup=True, reconf_funcs=None, dpid=True):
"""HUP and verify the HUP was processed."""
var = 'faucet_config_reload_warm_total'
if cold_start:
var = 'faucet_config_reload_cold_total'
old_count = int(
- self.scrape_prometheus_var(var, dpid=True, default=0))
+ self.scrape_prometheus_var(var, dpid=dpid, default=0))
start_configure_count = self.get_configure_count()
if reconf_funcs is None:
reconf_funcs = []
@@ -2073,7 +2073,7 @@ dbs:
if change_expected:
for _ in range(timeout):
new_count = int(
- self.scrape_prometheus_var(var, dpid=True, default=0))
+ self.scrape_prometheus_var(var, dpid=dpid, default=0))
if new_count > old_count:
break
time.sleep(1)
@@ -2082,7 +2082,7 @@ dbs:
msg='%s did not increment: %u' % (var, new_count))
else:
new_count = int(
- self.scrape_prometheus_var(var, dpid=True, default=0))
+ self.scrape_prometheus_var(var, dpid=dpid, default=0))
self.assertEqual(
old_count, new_count,
msg='%s incremented: %u' % (var, new_count))
diff --git a/clib/mininet_test_base_topo.py b/clib/mininet_test_base_topo.py
index 2c8e3838..22f17c08 100644
--- a/clib/mininet_test_base_topo.py
+++ b/clib/mininet_test_base_topo.py
@@ -112,7 +112,7 @@ class FaucetTopoTestBase(FaucetTestBase):
def build_net(self, host_links=None, host_vlans=None, switch_links=None,
link_vlans=None, mininet_host_options=None,
- n_vlans=1, acl_options=None, dp_options=None, host_options=None,
+ n_vlans=1, dp_options=None, host_options=None,
link_options=None, vlan_options=None, routers=None, router_options=None,
include=None, include_optional=None):
"""
@@ -123,7 +123,6 @@ class FaucetTopoTestBase(FaucetTestBase):
link_vlans (dict): Link tuple of switch indices (u, v) mapping to vlans
mininet_host_options (dict): Host index map to additional mininet host options
n_vlans (int): Number of VLANs to generate
- acl_options (dict): Acls in use in the Faucet configuration file
dp_options (dict): Additional options for each DP, keyed by DP index
host_options (dict): Additional options for each host, keyed by host index
link_options (dict): Additional options for each link, keyed by indices tuple (u, v)
@@ -159,30 +158,24 @@ class FaucetTopoTestBase(FaucetTestBase):
name = self.topo.switches_by_id[i]
dpid_names[dpid] = name
self.set_dpid_names(dpid_names)
+ self.configuration_options = {
+ 'acl_options': self.acls(),
+ 'dp_options': dp_options,
+ 'host_options': host_options,
+ 'link_options': link_options,
+ 'vlan_options': vlan_options,
+ 'routers': routers,
+ 'router_options': router_options,
+ 'include': include,
+ 'include_optional': include_optional
+ }
self.CONFIG = self.topo.get_config(
n_vlans,
- acl_options=self.acls(),
- dp_options=dp_options,
- host_options=host_options,
- link_options=link_options,
- vlan_options=vlan_options,
- routers=routers,
- router_options=router_options,
- include=include,
- include_optional=include_optional
+ **self.configuration_options
)
self.n_vlans = n_vlans
- self.routers = routers
- self.configuration_options = {
- 'vlan': vlan_options,
- 'acl': acl_options,
- 'dp': dp_options,
- 'host': host_options,
- 'link': link_options,
- 'router': router_options,
- 'host_vlans': host_vlans,
- 'link_vlans': link_vlans
- }
+ self.host_vlans = host_vlans
+ self.link_vlans = link_vlans
def start_net(self):
"""
@@ -194,7 +187,7 @@ class FaucetTopoTestBase(FaucetTestBase):
self.host_information = {}
for host_id, host_name in self.topo.hosts_by_id.items():
host = self.net.get(host_name)
- vlan = self.configuration_options['host_vlans'][host_id]
+ vlan = self.host_vlans[host_id]
ip_interface = ipaddress.ip_interface(self.host_ip_address(host_id, vlan))
self.set_host_ip(host, ip_interface)
self.host_information[host_id] = {
@@ -216,7 +209,7 @@ class FaucetTopoTestBase(FaucetTestBase):
def setup_lacp_bonds(self):
"""Search through host options for lacp hosts and configure accordingly"""
- host_options = self.configuration_options['host']
+ host_options = self.configuration_options['host_options']
if not host_options:
return
bond_index = 1
@@ -259,7 +252,7 @@ class FaucetTopoTestBase(FaucetTestBase):
def setup_intervlan_host_routes(self):
"""Configure host routes between hosts that belong on routed VLANs"""
- if self.routers:
+ if self.configuration_options['routers']:
for src in self.host_information:
src_host = self.host_information[src]['host']
src_vlan = self.host_information[src]['vlan']
@@ -363,6 +356,20 @@ class FaucetTopoTestBase(FaucetTestBase):
time.sleep(1)
self.fail('not enough links up: %f / %f' % (links_up, links))
+ def verify_stack_down(self):
+ """Verify all stack ports are down"""
+ links = 0
+ links_down = 0
+ for link, ports in self.link_port_maps.items():
+ for port in ports:
+ dpid = self.topo.dpids_by_id[link[0]]
+ name = self.topo.switches_by_id[link[0]]
+ status = self.stack_port_status(dpid, name, port)
+ links += 1
+ if status != 3:
+ links_down += 1
+ self.assertEqual(links, links_down, 'Not all links DOWN')
+
def verify_one_stack_down(self, stack_offset_port, coldstart=False):
"""Test conditions when one stack port is down"""
self.retry_net_ping()
@@ -470,7 +477,7 @@ class FaucetTopoTestBase(FaucetTestBase):
int_hosts = []
ext_hosts = []
dp_hosts = {self.topo.switches_by_id[dp_index]: ([], []) for dp_index in range(self.NUM_DPS)}
- for host_id, options in self.configuration_options['host'].items():
+ for host_id, options in self.configuration_options['host_options'].items():
host = self.host_information[host_id]['host']
if options.get('loop_protect_external', False):
ext_hosts.append(host)
@@ -561,8 +568,8 @@ class FaucetTopoTestBase(FaucetTestBase):
def is_routed_vlans(self, vlan_a, vlan_b):
"""Return true if the two vlans share a router"""
- if self.routers:
- for vlans in self.routers.values():
+ if self.configuration_options['routers']:
+ for vlans in self.configuration_options['routers'].values():
if (vlan_a in vlans and vlan_b in vlans):
return True
return False
@@ -587,7 +594,7 @@ class FaucetTopoTestBase(FaucetTestBase):
def get_expected_synced_states(self, host_id):
"""Return the list of regex string for the expected sync state of a LACP LAG connection"""
synced_state_list = []
- oper_key = self.configuration_options['host'][host_id]['lacp']
+ oper_key = self.configuration_options['host_options'][host_id]['lacp']
lacp_ports = [
port for ports in self.host_information[host_id]['ports'].values() for port in ports]
for port in lacp_ports:
@@ -625,7 +632,7 @@ details partner lacp pdu:
def prom_lacp_up_ports(self, dpid):
"""Get the number of up LAG ports according to Prometheus for a dpid"""
lacp_up_ports = 0
- for host_id, options in self.configuration_options['host'].items():
+ for host_id, options in self.configuration_options['host_options'].items():
# Find LACP hosts
for key in options.keys():
if key == 'lacp':
@@ -710,7 +717,7 @@ details partner lacp pdu:
def verify_lag_host_connectivity(self):
"""Verify LAG hosts can connect to any other host using the interface"""
# Find all LACP hosts
- for lacp_id, host_options in self.configuration_options['host'].items():
+ for lacp_id, host_options in self.configuration_options['host_options'].items():
if 'lacp' in host_options:
# Found LACP host
for dst_id in self.host_information:
diff --git a/faucet/conf.py b/faucet/conf.py
index c6179340..19f6b37f 100644
--- a/faucet/conf.py
+++ b/faucet/conf.py
@@ -132,7 +132,7 @@ class Conf:
continue
if isinstance(value, (tuple, list, set)) and isinstance(value[0], Conf):
continue
- conf_keys.append((key, value))
+ conf_keys.append((key, self._str_conf(value)))
return conf_keys
@staticmethod
diff --git a/faucet/dp.py b/faucet/dp.py
index d3df5673..5c85c3be 100644
--- a/faucet/dp.py
+++ b/faucet/dp.py
@@ -806,6 +806,14 @@ configuration.
"""Remove a stack link to the stack graph."""
return cls.modify_stack_topology(graph, dp, port, False)
+ @staticmethod
+ def hash_graph(graph):
+ """Return hash of a topology graph"""
+ # Using the degree of the topology is a quick way to get an estimate on
+ # whether a graph is isomorphic which is what we would really want when comparing
+ # two graphs.
+ return hash(tuple(sorted(graph.degree())))
+
def resolve_stack_topology(self, dps, meta_dp_state):
"""Resolve inter-DP config for stacking."""
stack_dps = [dp for dp in dps if dp.stack is not None]
@@ -1472,6 +1480,19 @@ configuration.
changed_acl_ports = set()
all_ports_changed = False
+ topology_changed = False
+ if self.stack_graph:
+ topology_changed = bool(self.hash_graph(self.stack_graph) != self.hash_graph(new_dp.stack_graph))
+ if topology_changed:
+ # Topology changed so restart stack ports just to be safe
+ stack_ports = [
+ port.number for port in new_dp.stack_ports
+ if port.number not in deleted_ports and
+ port.number not in added_ports]
+ changed_ports.update(set(stack_ports))
+ logger.info('Stack topology change detected, restarting stack ports')
+ same_ports -= changed_ports
+
if not same_ports:
all_ports_changed = True
# TODO: optimize case where only VLAN ACL changed.
@@ -1611,7 +1632,9 @@ configuration.
deleted_vlans (set): deleted VLAN IDs.
changed_vlans (set): changed/added VLAN IDs.
all_ports_changed (bool): True if all ports changed.
+ all_meters_changed (bool): True if all meters changed
deleted_meters (set): deleted meter numbers
+ added_meters (set): Added meter numbers
changed_meters (set): changed/added meter numbers
"""
if new_dp._table_configs() != self._table_configs():
diff --git a/faucet/port.py b/faucet/port.py
index a5eb01c8..927390f2 100644
--- a/faucet/port.py
+++ b/faucet/port.py
@@ -26,6 +26,14 @@ STACK_STATE_BAD = 2
STACK_STATE_UP = 3
STACK_STATE_GONE = 4
STACK_STATE_NONE = -1
+STACK_DISPLAY_DICT = {
+ STACK_STATE_ADMIN_DOWN: 'ADMIN_DOWN',
+ STACK_STATE_INIT: 'INITIALIZING',
+ STACK_STATE_BAD: 'BAD',
+ STACK_STATE_UP: 'UP',
+ STACK_STATE_GONE: 'GONE',
+ STACK_STATE_NONE: 'NONE'
+}
# LACP not configured
LACP_ACTOR_NOTCONFIGURED = -1
@@ -686,3 +694,7 @@ class Port(Conf):
def stack_gone(self):
"""Change the current stack state to GONE."""
self.dyn_stack_current_state = STACK_STATE_GONE
+
+ def stack_state_name(self, state):
+ """Return stack state name"""
+ return STACK_DISPLAY_DICT[state]
diff --git a/faucet/valve.py b/faucet/valve.py
index 8a106965..1c73d3b6 100644
--- a/faucet/valve.py
+++ b/faucet/valve.py
@@ -1203,7 +1203,7 @@ class Valve:
if remote_port_state:
self.logger.info('LLDP on %s, %s from %s (remote %s, port %u) state %s' % (
chassis_id, port, pkt_meta.eth_src, valve_util.dpid_log(remote_dp_id),
- remote_port_id, remote_port_state))
+ remote_port_id, port.stack_state_name(remote_port_state)))
port.dyn_lldp_beacon_recv_state = remote_port_state
peer_mac_src = self.dp.ports[port.number].lldp_peer_mac
diff --git a/tests/integration/mininet_multidp_tests.py b/tests/integration/mininet_multidp_tests.py
index c1a0e5c1..32b57efc 100644
--- a/tests/integration/mininet_multidp_tests.py
+++ b/tests/integration/mininet_multidp_tests.py
@@ -149,7 +149,7 @@ class FaucetMultiDPTest(FaucetTopoTestBase):
# Create VLAN configuration options
vlan_options = {}
if routers:
- for vlans in self.routers():
+ for vlans in routers:
for vlan in vlans:
if vlan not in vlan_options:
vlan_options[vlan] = {
@@ -492,7 +492,7 @@ class FaucetSingleStackStringOfDPExtLoopProtUntaggedTest(FaucetMultiDPTest):
conf['dps'][dp_name]['interfaces'][loop_intf]['loop_protect_external'] = protect_external
self.reload_conf(
conf, self.faucet_config_path,
- restart=True, cold_start=False, change_expected=True)
+ restart=True, cold_start=False, change_expected=True, dpid=self.topo.dpids_by_id[1])
def test_missing_ext(self):
"""Test stacked dp with all external ports down on a switch"""
@@ -894,7 +894,7 @@ class FaucetSingleStackOrderedAclControlTest(FaucetMultiDPTest):
class FaucetStringOfDPACLOverrideTest(FaucetMultiDPTest):
"""Test overriding ACL rules"""
- NUM_DPS = 1
+ NUM_DPS = 2
NUM_HOSTS = 2
SOFTWARE_ONLY = True
@@ -966,12 +966,16 @@ class FaucetStringOfDPACLOverrideTest(FaucetMultiDPTest):
}
def include_optional(self):
+ if self.acls_config is None:
+ self.acls_config = os.path.join(self.tmpdir, 'acls.yaml')
+ if self.missing_config is None:
+ self.missing_config = os.path.join(self.tmpdir, 'missing_config.yaml')
return [self.acls_config, self.missing_config]
def setUp(self): # pylint: disable=invalid-name
"""Setup network & create configuration file"""
- self.acls_config = os.path.join(self.tmpdir, 'acls.yaml')
- self.missing_config = os.path.join(self.tmpdir, 'missing_config.yaml')
+ self.acls_config = None
+ self.missing_config = None
super(FaucetStringOfDPACLOverrideTest, self).set_up(
n_dps=self.NUM_DPS,
n_untagged=self.NUM_HOSTS)
@@ -982,10 +986,10 @@ class FaucetStringOfDPACLOverrideTest(FaucetMultiDPTest):
first_host, second_host = self.hosts_name_ordered()[0:2]
self.verify_tp_dst_notblocked(5001, first_host, second_host)
with open(self.acls_config, 'w') as config_file:
- config_file.write(self.topo.get_config(n_vlans=1, acl_options=self.acls_override()))
+ self.configuration_options['acl_options'] = self.acls_override()
+ config_file.write(self.topo.get_config(n_vlans=1, **self.configuration_options))
self.verify_faucet_reconf(cold_start=False, change_expected=True)
self.verify_tp_dst_blocked(5001, first_host, second_host)
- self.verify_no_cable_errors()
def test_port5002_notblocked(self):
"""Test that TCP port 5002 is not blocked."""
@@ -993,10 +997,10 @@ class FaucetStringOfDPACLOverrideTest(FaucetMultiDPTest):
first_host, second_host = self.hosts_name_ordered()[0:2]
self.verify_tp_dst_blocked(5002, first_host, second_host)
with open(self.acls_config, 'w') as config_file:
- config_file.write(self.topo.get_config(n_vlans=1, acl_options=self.acls_override()))
+ self.configuration_options['acl_options'] = self.acls_override()
+ config_file.write(self.topo.get_config(n_vlans=1, **self.configuration_options))
self.verify_faucet_reconf(cold_start=False, change_expected=True)
self.verify_tp_dst_notblocked(5002, first_host, second_host)
- self.verify_no_cable_errors()
class FaucetTunnelSameDpTest(FaucetMultiDPTest):
@@ -2096,3 +2100,215 @@ class FaucetStackTopoChangeTest(FaucetMultiDPTest):
self.assertEqual(node_count, 3,
'Number of nodes in graph object is %s (!=3)' % node_count)
self.assertTrue(stack_event_found)
+
+
+class FaucetStackWarmStartTest(FaucetTopoTestBase):
+ """Test various stack warm starting conditions to ensure stack port stays UP"""
+
+ NUM_DPS = 3
+ NUM_HOSTS = 1
+ NUM_VLANS = 2
+ SOFTWARE_ONLY = True
+
+ def setUp(self):
+ """Ignore to allow for setting up network in each test"""
+
+ def set_up(self, host_links=None, host_vlans=None, switch_to_switch_links=1):
+ """
+ Args:
+ host_links (dict): Host index map to list of DPs it is connected to
+ host_vlans (dict): Host index map to list of vlans it belongs to
+ """
+ super().setUp()
+ network_graph = networkx.path_graph(self.NUM_DPS)
+ dp_options = {}
+ for dp_i in network_graph.nodes():
+ dp_options.setdefault(dp_i, {
+ 'group_table': self.GROUP_TABLE,
+ 'ofchannel_log': self.debug_log_path + str(dp_i) if self.debug_log_path else None,
+ 'hardware': self.hardware if dp_i == 0 and self.hw_dpid else 'Open vSwitch'
+ })
+ if dp_i == 0:
+ dp_options[0]['stack'] = {'priority': 1}
+ switch_links = list(network_graph.edges()) * switch_to_switch_links
+ link_vlans = {edge: None for edge in switch_links}
+ if host_links is None:
+ host_links = {0: [0], 1: [1], 2: [2]}
+ if host_vlans is None:
+ host_vlans = {h_i: 0 for h_i in host_links.keys()}
+ self.build_net(
+ host_links=host_links,
+ host_vlans=host_vlans,
+ switch_links=switch_links,
+ link_vlans=link_vlans,
+ n_vlans=self.NUM_VLANS,
+ dp_options=dp_options,
+ )
+ self.start_net()
+
+ def test_native_vlan(self):
+ """Test warm starting changing host native VLAN"""
+ host_vlans = {0: 0, 1: 0, 2: 1}
+ self.set_up(host_vlans=host_vlans)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[2]]['interfaces']
+ interfaces_conf[self.host_port_maps[2][2][0]]['native_vlan'] = self.topo.vlan_name(0)
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=False, change_expected=True, dpid=self.topo.dpids_by_id[2])
+ self.verify_stack_up(timeout=1)
+ self.verify_intervlan_routing()
+
+ def test_vlan_change(self):
+ """Test warm starting changing a VLAN option"""
+ host_vlans = {0: 0, 1: 0, 2: 1}
+ self.set_up(host_vlans=host_vlans)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ conf['vlans'][self.topo.vlan_name(0)]['edge_learn_stack_root'] = False
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=False, change_expected=True)
+ self.verify_stack_up(timeout=1)
+ self.verify_intervlan_routing()
+
+ def test_transit_vlan_change(self):
+ """Test warm starting changing host native VLAN with a transit stack switch"""
+ import ipaddress
+ host_links = {0: [0], 1: [0], 2: [2], 3: [2]}
+ host_vlans = {0: 0, 1: 0, 2: 0, 3: 1}
+ self.set_up(host_links=host_links, host_vlans=host_vlans)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[0]]['interfaces']
+ interfaces_conf[self.host_port_maps[0][0][0]]['native_vlan'] = self.topo.vlan_name(1)
+ self.host_information[0]['vlan'] = 1
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=False, change_expected=True, dpid=self.topo.dpids_by_id[0])
+ self.verify_stack_up(timeout=1)
+ ip_intf = ipaddress.ip_interface(self.host_ip_address(0, 1))
+ self.host_information[0]['ip'] = ip_intf
+ self.set_host_ip(self.host_information[0]['host'], ip_intf)
+ self.verify_intervlan_routing()
+
+ def test_del_seconday_stack_port(self):
+ """Test deleting stack port"""
+ self.set_up(switch_to_switch_links=2)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ del conf['dps'][self.topo.switches_by_id[1]]['interfaces'][self.link_port_maps[(1, 2)][0]]
+ del conf['dps'][self.topo.switches_by_id[2]]['interfaces'][self.link_port_maps[(2, 1)][0]]
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=False, change_expected=True, dpid=self.topo.dpids_by_id[1])
+ self.verify_stack_up(timeout=1)
+ self.verify_intervlan_routing()
+
+ def test_del_primary_stack_port(self):
+ """Test deleting lowest/primary stack port"""
+ self.set_up(switch_to_switch_links=2)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ del conf['dps'][self.topo.switches_by_id[1]]['interfaces'][self.link_port_maps[(1, 2)][1]]
+ del conf['dps'][self.topo.switches_by_id[2]]['interfaces'][self.link_port_maps[(2, 1)][1]]
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=False, change_expected=True, dpid=self.topo.dpids_by_id[1])
+ self.verify_stack_up(timeout=1)
+ self.verify_intervlan_routing()
+
+ def test_del_host(self):
+ """Test removing a port/host from Faucet"""
+ host_links = {0: [0], 1: [0], 2: [1], 3: [2]}
+ self.set_up(host_links=host_links)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[0]]['interfaces']
+ del interfaces_conf[self.host_port_maps[0][0][0]]
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=False, change_expected=True, dpid=self.topo.dpids_by_id[0])
+ self.verify_stack_up(timeout=1)
+ del self.host_information[0]
+ self.verify_intervlan_routing()
+
+ def test_root_add_stack_link(self):
+ """Add a redundant stack link between two switches (one a root)"""
+ host_links = {0: [0], 1: [0], 2: [1], 3: [1], 4: [2], 5: [2]}
+ self.set_up(host_links=host_links)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ # Create an additional link between S1-S2
+ port_num = self.topo._create_next_port(self.topo.switches_by_id[0])
+ rev_port_num = self.topo._create_next_port(self.topo.switches_by_id[1])
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[0]]['interfaces']
+ interfaces_conf[port_num] = {
+ 'name': 'b%u' % port_num,
+ 'stack': {'dp': self.topo.switches_by_id[1], 'port': rev_port_num}}
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[1]]['interfaces']
+ interfaces_conf[rev_port_num] = {
+ 'name': 'b%u' % rev_port_num,
+ 'stack': {'dp': self.topo.switches_by_id[0], 'port': port_num}}
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=False, change_expected=True, dpid=self.topo.dpids_by_id[1])
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+
+ def test_add_stack_link(self):
+ """Add a redundant stack link between two non-root switches"""
+ host_links = {0: [0], 1: [0], 2: [1], 3: [1], 4: [2], 5: [2]}
+ self.set_up(host_links=host_links)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ # Create an additional link between S1-S2
+ port_num = self.topo._create_next_port(self.topo.switches_by_id[1])
+ rev_port_num = self.topo._create_next_port(self.topo.switches_by_id[2])
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[1]]['interfaces']
+ interfaces_conf[port_num] = {
+ 'name': 'b%u' % port_num,
+ 'stack': {'dp': self.topo.switches_by_id[2], 'port': rev_port_num}}
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[2]]['interfaces']
+ interfaces_conf[rev_port_num] = {
+ 'name': 'b%u' % rev_port_num,
+ 'stack': {'dp': self.topo.switches_by_id[1], 'port': port_num}}
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=False, change_expected=True, dpid=self.topo.dpids_by_id[1])
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+
+ def test_add_stack_link_transit(self):
+ """Add a redundant stack link between two non-root switches (with a transit switch)"""
+ host_links = {0: [0], 1: [0], 4: [2], 5: [2]}
+ self.set_up(host_links=host_links)
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
+ conf = self._get_faucet_conf()
+ # Create an additional link between S1-S2
+ port_num = self.topo._create_next_port(self.topo.switches_by_id[1])
+ rev_port_num = self.topo._create_next_port(self.topo.switches_by_id[2])
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[1]]['interfaces']
+ interfaces_conf[port_num] = {
+ 'name': 'b%u' % port_num,
+ 'stack': {'dp': self.topo.switches_by_id[2], 'port': rev_port_num}}
+ interfaces_conf = conf['dps'][self.topo.switches_by_id[2]]['interfaces']
+ interfaces_conf[rev_port_num] = {
+ 'name': 'b%u' % rev_port_num,
+ 'stack': {'dp': self.topo.switches_by_id[1], 'port': port_num}}
+ # Expected cold start as topology changed with all ports being stack ports
+ self.reload_conf(
+ conf, self.faucet_config_path, restart=True,
+ cold_start=True, change_expected=True, dpid=self.topo.dpids_by_id[1])
+ self.verify_stack_up()
+ self.verify_intervlan_routing()
diff --git a/tests/unit/faucet/test_valve_stack.py b/tests/unit/faucet/test_valve_stack.py
index 9b10374b..71bfa1f5 100755
--- a/tests/unit/faucet/test_valve_stack.py
+++ b/tests/unit/faucet/test_valve_stack.py
@@ -303,7 +303,7 @@ dps:
"""Return other running valves"""
return self.valves_manager._other_running_valves(valve) # pylint: disable=protected-access
- def test_MCLAG_cold_start(self):
+ def test_mclag_cold_start(self):
"""Test cold-starting a switch with a downed port resets LACP states"""
self.activate_all_ports()
valve = self.valves_manager.valves[0x1]
@@ -377,7 +377,7 @@ dps:
"""Return other running valves"""
return self.valves_manager._other_running_valves(valve) # pylint: disable=protected-access
- def test_MCLAG_standby_option(self):
+ def test_mclag_standby_option(self):
"""Test MCLAG standby option forces standby state instead of unselected"""
self.activate_all_ports()
valve = self.valves_manager.valves[0x1]
@@ -540,7 +540,7 @@ class ValveStackChainTest(ValveTestBases.ValveTestSmall):
def test_stack_learn_not_root(self):
"""Test stack learned when not root"""
- self.update_config(self._config_edge_learn_stack_root(False))
+ self.update_config(self._config_edge_learn_stack_root(False), reload_type='warm')
self.activate_all_ports()
self.validate_edge_learn_ports()
@@ -617,7 +617,7 @@ class ValveStackEdgeLearnTestCase(ValveStackLoopTest):
def test_edge_learn_edge_port(self):
"""Check the behavior of the basic edge_learn_port algorithm"""
- self.update_config(self._config_edge_learn_stack_root(False))
+ self.update_config(self._config_edge_learn_stack_root(False), reload_type='warm')
self.activate_all_ports()
@@ -2379,5 +2379,210 @@ dps:
ofmsgs + [global_flowmod, global_metermod, global_groupmod]), False)
+class ValveWarmStartStackTest(ValveTestBases.ValveTestSmall):
+ """Test warm starting stack ports"""
+
+ CONFIG = """
+vlans:
+ vlan100:
+ vid: 100
+ vlan200:
+ vid: 200
+dps:
+ s1:
+ dp_id: 0x1
+ hardware: 'GenericTFM'
+ stack: {priority: 1}
+ interfaces:
+ 1:
+ stack: {dp: s2, port: 1}
+ 2:
+ name: host1
+ native_vlan: vlan100
+ 3:
+ name: host2
+ native_vlan: vlan200
+ 4:
+ name: host3
+ native_vlan: vlan200
+ s2:
+ dp_id: 0x2
+ hardware: 'GenericTFM'
+ interfaces:
+ 1:
+ stack: {dp: s1, port: 1}
+ 2:
+ stack: {dp: s3, port: 1}
+ 4:
+ name: host4
+ native_vlan: vlan100
+ 5:
+ name: host5
+ native_vlan: vlan200
+ s3:
+ dp_id: 0x3
+ hardware: 'GenericTFM'
+ interfaces:
+ 1:
+ stack: {dp: s2, port: 2}
+ 3:
+ name: host6
+ native_vlan: vlan100
+ 4:
+ name: host7
+ native_vlan: vlan200
+"""
+
+ NEW_PORT_CONFIG = """
+vlans:
+ vlan100:
+ vid: 100
+ vlan200:
+ vid: 200
+dps:
+ s1:
+ dp_id: 0x1
+ hardware: 'GenericTFM'
+ stack: {priority: 1}
+ interfaces:
+ 1:
+ stack: {dp: s2, port: 1}
+ 2:
+ name: host1
+ native_vlan: vlan100
+ 3:
+ name: host2
+ native_vlan: vlan200
+ 4:
+ name: host3
+ native_vlan: vlan200
+ s2:
+ dp_id: 0x2
+ hardware: 'GenericTFM'
+ interfaces:
+ 1:
+ stack: {dp: s1, port: 1}
+ 2:
+ stack: {dp: s3, port: 1}
+ 3:
+ stack: {dp: s3, port: 2}
+ 4:
+ name: host4
+ native_vlan: vlan100
+ 5:
+ name: host5
+ native_vlan: vlan200
+ s3:
+ dp_id: 0x3
+ hardware: 'GenericTFM'
+ interfaces:
+ 1:
+ stack: {dp: s2, port: 2}
+ 2:
+ stack: {dp: s2, port: 3}
+ 3:
+ name: host6
+ native_vlan: vlan100
+ 4:
+ name: host7
+ native_vlan: vlan200
+"""
+
+ NEW_VLAN_CONFIG = """
+vlans:
+ vlan100:
+ vid: 100
+ vlan200:
+ vid: 200
+dps:
+ s1:
+ dp_id: 0x1
+ hardware: 'GenericTFM'
+ stack: {priority: 1}
+ interfaces:
+ 1:
+ stack: {dp: s2, port: 1}
+ 2:
+ name: host1
+ native_vlan: vlan100
+ 3:
+ name: host2
+ native_vlan: vlan100
+ 4:
+ name: host3
+ native_vlan: vlan200
+ s2:
+ dp_id: 0x2
+ hardware: 'GenericTFM'
+ interfaces:
+ 1:
+ stack: {dp: s1, port: 1}
+ 2:
+ stack: {dp: s3, port: 1}
+ 4:
+ name: host4
+ native_vlan: vlan100
+ 5:
+ name: host5
+ native_vlan: vlan200
+ s3:
+ dp_id: 0x3
+ hardware: 'GenericTFM'
+ interfaces:
+ 1:
+ stack: {dp: s2, port: 2}
+ 3:
+ name: host6
+ native_vlan: vlan100
+ 4:
+ name: host7
+ native_vlan: vlan200
+"""
+
+ def setUp(self):
+ """Setup network and start stack ports"""
+ self.setup_valve(self.CONFIG)
+ self.process_ports()
+
+ def process_ports(self):
+ """Makes sure all ports are up and flowrules installed"""
+ self.activate_all_ports()
+ for valve in self.valves_manager.valves.values():
+ for port in valve.dp.ports.values():
+ if port.stack:
+ self.set_stack_port_up(port.number, valve)
+
+ def test_reload_topology_change(self):
+ """Test reload with topology change forces stack ports down"""
+ self.update_and_revert_config(
+ self.CONFIG, self.NEW_PORT_CONFIG,
+ 'warm', verify_func=self.process_ports)
+ with open(self.config_file, 'w') as config_file:
+ config_file.write(self.NEW_PORT_CONFIG)
+ new_dps = self.valves_manager.parse_configs(self.config_file)
+ for new_dp in new_dps:
+ valve = self.valves_manager.valves[new_dp.dp_id]
+ changes = valve.dp.get_config_changes(valve.logger, new_dp)
+ changed_ports, all_ports_changed = changes[1], changes[6]
+ for port in valve.dp.stack_ports:
+ if not all_ports_changed:
+ self.assertIn(
+ port.number, changed_ports,
+ 'Stack port not detected as changed on topology change')
+
+ def test_reload_vlan_change(self):
+ """Test reload with topology change stack ports stay up"""
+ with open(self.config_file, 'w') as config_file:
+ config_file.write(self.NEW_VLAN_CONFIG)
+ new_dps = self.valves_manager.parse_configs(self.config_file)
+ for new_dp in new_dps:
+ valve = self.valves_manager.valves[new_dp.dp_id]
+ changed_ports = valve.dp.get_config_changes(valve.logger, new_dp)[1]
+ for port in valve.dp.stack_ports:
+ self.assertNotIn(
+ port.number, changed_ports,
+ 'Stack port detected as changed on non-topology change')
+
+
if __name__ == "__main__":
unittest.main() # pytype: disable=module-attr
diff --git a/tests/unit/faucet/valve_test_lib.py b/tests/unit/faucet/valve_test_lib.py
index 23a8799e..5e8e7d90 100644
--- a/tests/unit/faucet/valve_test_lib.py
+++ b/tests/unit/faucet/valve_test_lib.py
@@ -642,6 +642,7 @@ class ValveTestBases:
self.update_config(orig_config, reload_type)
final_table_state = str(self.table)
diff = difflib.unified_diff(before_table_state.splitlines(), str(final_table_state).splitlines())
+ self.maxDiff = None
self.assertEqual(before_table_state, final_table_state, msg='\n'.join(diff))
def connect_dp(self):
@@ -947,7 +948,9 @@ class ValveTestBases:
port.dyn_stack_current_state = status
valve.switch_manager.update_stack_topo(True, valve.dp, port)
for valve_vlan in valve.dp.vlans.values():
- self.apply_ofmsgs(valve.switch_manager.add_vlan(valve_vlan))
+ ofmsgs = valve.switch_manager.add_vlan(valve_vlan)
+ if valve is self.valve:
+ self.apply_ofmsgs(ofmsgs)
def set_stack_port_up(self, port_no, valve=None):
"""Set stack port up recalculating topology as necessary."""