diff --git a/clib/clib_mininet_test_main.py b/clib/clib_mininet_test_main.py index 0a0e667bc8..ac604d04ee 100755 --- a/clib/clib_mininet_test_main.py +++ b/clib/clib_mininet_test_main.py @@ -152,8 +152,8 @@ def import_hw_config(): valid_types, config_file_name)) sys.exit(-1) dp_ports = config['dp_ports'] - if len(dp_ports) != REQUIRED_TEST_PORTS: - print('Exactly %u dataplane ports are required, ' + if len(dp_ports) < REQUIRED_TEST_PORTS: + print('At least %u dataplane ports are required, ' '%d are provided in %s.' % (REQUIRED_TEST_PORTS, len(dp_ports), config_file_name)) sys.exit(-1) @@ -680,6 +680,10 @@ def parse_args(): '-d', '--dumpfail', action='store_true', help='dump logs for failed tests') parser.add_argument( '-k', '--keep_logs', action='store_true', help='keep logs even for OK tests') + loglevels = ('debug', 'error', 'warning', 'info', 'output') + parser.add_argument( + '-l', '--loglevel', choices=loglevels, default='warning', + help='set mininet logging level') parser.add_argument( '-n', '--nocheck', action='store_true', help='skip dependency check') parser.add_argument( @@ -722,16 +726,19 @@ def parse_args(): return ( requested_test_classes, args.clean, args.dumpfail, args.keep_logs, args.nocheck, args.serial, args.repeat, - excluded_test_classes, report_json_filename, port_order) + excluded_test_classes, report_json_filename, port_order, + args.loglevel) def test_main(module): """Test main.""" - setLogLevel('error') print('testing module %s' % module) (requested_test_classes, clean, dumpfail, keep_logs, nocheck, - serial, repeat, excluded_test_classes, report_json_filename, port_order) = parse_args() + serial, repeat, excluded_test_classes, report_json_filename, port_order, + loglevel) = parse_args() + + setLogLevel(loglevel) if clean: print('Cleaning up test interfaces, processes and openvswitch ' diff --git a/clib/mininet_test_base.py b/clib/mininet_test_base.py index d842b52af3..3caea5b959 100644 --- a/clib/mininet_test_base.py +++ b/clib/mininet_test_base.py @@ -26,9 +26,10 @@ from ryu.ofproto import ofproto_v1_3 as ofp -from mininet.log import error, output # pylint: disable=import-error -from mininet.net import Mininet # pylint: disable=import-error -from mininet.util import dumpNodeConnections, pmonitor # pylint: disable=import-error +from mininet.link import Intf as HWIntf # pylint: disable=import-error +from mininet.log import error, output # pylint: disable=import-error +from mininet.net import Mininet # pylint: disable=import-error +from mininet.util import dumpNodeConnections, pmonitor # pylint: disable=import-error from clib import mininet_test_util from clib import mininet_test_topo @@ -374,7 +375,9 @@ def tearDown(self, ignore_oferrors=False): 'dump-flows', 'dump-groups', 'dump-meters', 'dump-group-stats', 'dump-ports', 'dump-ports-desc'): switch_dump_name = os.path.join(self.tmpdir, '%s-%s.log' % (switch.name, dump_cmd)) - switch.cmd('%s %s %s > %s' % (self.OFCTL, dump_cmd, switch.name, switch_dump_name)) + # This seems to fail occasionally, so we warn on error rather than quitting + switch.cmd('%s %s %s > %s' % (self.OFCTL, dump_cmd, switch.name, switch_dump_name), + success=None) for other_cmd in ('show', 'list controller', 'list manager'): other_dump_name = os.path.join(self.tmpdir, '%s.log' % other_cmd.replace(' ', '')) switch.cmd('%s %s > %s' % (self.VSCTL, other_cmd, other_dump_name)) @@ -429,31 +432,49 @@ def _cmd(cmd): _cmd(cmd) def _attach_physical_switch(self): - """Bridge a physical switch into test topology.""" + """Bridge a physical switch into test topology. + + We do this for now to enable us to reconnect + virtual ethernet interfaces which may already + exist on emulated hosts and other OVS instances. + + (One alternative would be to create a Link() class + that uses the hardware interfaces directly.) + + We repurpose the first OvS switch in the topology + as a patch panel that transparently connects the + hardware interfaces to the host/switch veth links.""" switch = self.first_switch() - phys_macs = set() - mapped_base = len(self.switch_map) - for port_i, test_host_port in enumerate(sorted(self.switch_map), start=1): - mapped_port_i = mapped_base + port_i - phys_port = FaucetIntf(self.switch_map[test_host_port], node=switch) - phys_mac = self.get_mac_of_intf(phys_port.name) - self.assertFalse(phys_mac in phys_macs, 'duplicate physical MAC %s' % phys_mac) - phys_macs.add(phys_mac) - switch.cmd( - ('ovs-vsctl add-port %s %s -- ' - 'set Interface %s ofport_request=%u') % ( - switch.name, - phys_port.name, - phys_port.name, - mapped_port_i)) - switch.cmd('%s add-flow %s in_port=%u,eth_src=%s,priority=2,actions=drop' % ( - self.OFCTL, switch.name, mapped_port_i, phys_mac)) - switch.cmd('%s add-flow %s in_port=%u,eth_dst=%s,priority=2,actions=drop' % ( - self.OFCTL, switch.name, port_i, phys_mac)) - for port_pair in ((port_i, mapped_port_i), (mapped_port_i, port_i)): - in_port, out_port = port_pair - switch.cmd('%s add-flow %s in_port=%u,priority=1,actions=output:%u' % ( - self.OFCTL, switch.name, in_port, out_port)) + # hw_names are the names of the server hardware interfaces + # that are cabled to the device under test, sorted by OF port number + hw_names = [self.switch_map[port] for port in sorted(self.switch_map)] + hw_macs = set() + # ovs_ports are the (sorted) OF port numbers of the OvS interfaces + # that are already attached to the emulated network. + # The actual tests reorder them according to port_map + ovs_ports = sorted(self.topo.switch_ports[switch.name]) + # Patch hardware interfaces through to to OvS interfaces + for hw_name, ovs_port in zip(hw_names, ovs_ports): + # Note we've already removed any Linux IP addresses from hw_name + # and blocked traffic to/from its meaningless MAC + hw_mac = self.get_mac_of_intf(hw_name) + self.assertFalse(hw_mac in hw_macs, + 'duplicate hardware MAC %s' % hw_mac) + hw_macs.add(hw_mac) + # Create mininet Intf and attach it to the switch + hw_intf = HWIntf(hw_name, node=switch) + switch.attach(hw_intf) + hw_port = switch.ports[hw_intf] + # Connect hw_port <-> ovs_port + src, dst = hw_port, ovs_port + for flow in ( + # Drop anything to or from the meaningless hw_mac + 'eth_src=%s,priority=2,actions=drop' % hw_mac, + 'eth_dst=%s,priority=2,actions=drop' % hw_mac, + # Forward traffic bidirectionally src <-> dst + 'in_port=%u,priority=1,actions=output:%u' % (src, dst), + 'in_port=%u,priority=1,actions=output:%u' % (dst, src)): + switch.cmd(self.OFCTL, 'add-flow', switch, flow) def create_port_map(self, dpid): """Return a port map {'port_1': port...} for a dpid in self.topo""" @@ -1194,7 +1215,15 @@ def _prometheus_url(self, controller): self.get_prom_addr(), self.config_ports['gauge_prom_port']) raise NotImplementedError + _last_scrape_time = 0.0 # last scrape time + def scrape_prometheus(self, controller='faucet', timeout=15, var=None): + # Pause a bit if it has been less than a second since last request + min_interval = 1 + scrape_interval = time.time() - self._last_scrape_time + if scrape_interval < min_interval: + time.sleep(min_interval - scrape_interval) + self._last_scrape_time = time.time() url = self._prometheus_url(controller) try: prom_raw = requests.get(url, {}, timeout=timeout).text @@ -1229,16 +1258,24 @@ def wait_for_prometheus_var(self, var, result_wanted, labels=None, any_labels=Fa dpid=dpid, multiple=multiple, controller=controller, retries=retries) if result == result_wanted: return True - time.sleep(1) return False + _prometheus_cache = '' + def scrape_prometheus_var(self, var, labels=None, any_labels=False, default=None, - dpid=True, multiple=False, controller='faucet', retries=3): + dpid=True, multiple=False, controller='faucet', retries=3, + cache=False): + """Scrape a variable from prometheus + var: prometheus variable name + label: label if any + any_labels: accept any/all labels? (False) + default: default value to return if var is missing (None) + dpid: dpid | True (use self.dpid) | None (True) + controller: controller to scrape ('faucet') + retries: number of times to retry before giving up + cache: check cached results instead? (False)""" if dpid: - if dpid is True: - dpid = int(self.dpid) - else: - dpid = int(dpid) + dpid = int(self.dpid) if dpid is True else int(dpid) label_values_re = r'' if any_labels: label_values_re = r'\{[^\}]+\}' @@ -1254,10 +1291,17 @@ def scrape_prometheus_var(self, var, labels=None, any_labels=False, default=None label_values_re = r'\{%s\}' % r'\S+'.join(label_values) var_re = re.compile(r'^%s%s$' % (var, label_values_re)) for _ in range(retries): - results = [] - prom_lines = self.scrape_prometheus(controller, var=var) - for prom_line in prom_lines: - prom_var, prom_val = self.parse_prom_var(prom_line) + results, prom_lines = [], [] + if cache: + prom_lines = [line for line in self._prometheus_cache + if line.startswith(var)] + if not prom_lines: + prom_lines = self.scrape_prometheus(controller) + self._prometheus_cache = prom_lines + prom_lines = [line for line in prom_lines + if line.startswith(var)] + for line in prom_lines: + prom_var, prom_val = self.parse_prom_var(line) if var_re.match(prom_var): results.append((var, prom_val)) if not multiple: @@ -1266,7 +1310,8 @@ def scrape_prometheus_var(self, var, labels=None, any_labels=False, default=None if multiple: return results return results[0][1] - time.sleep(1) + if cache and prom_lines: + break return default def gauge_smoke_test(self): @@ -1312,7 +1357,6 @@ def get_configure_count(self, retries=5): 'faucet_config_reload_requests_total', default=None, dpid=False) if count: break - time.sleep(1) self.assertTrue(count, msg='configure count stayed zero') return count @@ -1434,7 +1478,6 @@ def verify_traveling_dhcp_mac(self, retries=10): new_locations.add(location) if locations != new_locations: break - time.sleep(1) # TODO: verify port/host association, not just that host moved. self.assertNotEqual(locations, new_locations) locations = new_locations @@ -1905,7 +1948,6 @@ def verify_faucet_reconf(self, timeout=10, configure_count = self.get_configure_count() if configure_count > start_configure_count: break - time.sleep(1) self.assertNotEqual( start_configure_count, configure_count, 'FAUCET did not reconfigure') if change_expected: @@ -1914,7 +1956,6 @@ def verify_faucet_reconf(self, timeout=10, self.scrape_prometheus_var(var, dpid=True, default=0)) if new_count > old_count: break - time.sleep(1) self.assertTrue( new_count > old_count, msg='%s did not increment: %u' % (var, new_count)) @@ -2002,7 +2043,6 @@ def wait_port_status(self, dpid, port_no, status, expected_status, timeout=10): if port_status is not None and port_status == expected_status: return self._portmod(dpid, port_no, status, ofp.OFPPC_PORT_DOWN) - time.sleep(1) self.fail('dpid %x port %s status %s != expected %u' % ( dpid, port_no, port_status, expected_status)) @@ -2026,26 +2066,30 @@ def wait_dp_status(self, expected_status, controller='faucet', timeout=30): return self.wait_for_prometheus_var( 'dp_status', expected_status, any_labels=True, controller=controller, default=None, timeout=timeout) - def _get_tableid(self, name): - return self.scrape_prometheus_var( - 'faucet_config_table_names', {'table_name': name}) - def quiet_commands(self, host, commands): for command in commands: result = host.cmd(command) self.assertEqual('', result, msg='%s: %s' % (command, result)) def _config_tableids(self): - self._PORT_ACL_TABLE = self._get_tableid('port_acl') - self._VLAN_TABLE = self._get_tableid('vlan') - self._VLAN_ACL_TABLE = self._get_tableid('vlan_acl') - self._ETH_SRC_TABLE = self._get_tableid('eth_src') - self._IPV4_FIB_TABLE = self._get_tableid('ipv4_fib') - self._IPV6_FIB_TABLE = self._get_tableid('ipv6_fib') - self._VIP_TABLE = self._get_tableid('vip') - self._ETH_DST_HAIRPIN_TABLE = self._get_tableid('eth_dst_hairpin') - self._ETH_DST_TABLE = self._get_tableid('eth_dst') - self._FLOOD_TABLE = self._get_tableid('flood') + def get_tableid(name, retries=1, cache=True): + return self.scrape_prometheus_var( + 'faucet_config_table_names', + {'table_name': name}, retries=retries, cache=cache) + # Always-there tables + self._VLAN_TABLE = get_tableid('vlan', retries=3, cache=False) + self._ETH_SRC_TABLE = get_tableid('eth_src') + self._ETH_DST_TABLE = get_tableid('eth_dst') + self._FLOOD_TABLE = get_tableid('flood') + self.assertTrue(None not in (self._VLAN_TABLE, self._ETH_SRC_TABLE, + self._ETH_DST_TABLE, self._FLOOD_TABLE)) + # Sometimes-there tables + self._PORT_ACL_TABLE = get_tableid('port_acl') + self._VLAN_ACL_TABLE = get_tableid('vlan_acl') + self._IPV4_FIB_TABLE = get_tableid('ipv4_fib') + self._IPV6_FIB_TABLE = get_tableid('ipv6_fib') + self._VIP_TABLE = get_tableid('vip') + self._ETH_DST_HAIRPIN_TABLE = get_tableid('eth_dst_hairpin') def _dp_ports(self): return list(sorted(self.port_map.values())) diff --git a/clib/mininet_test_topo.py b/clib/mininet_test_topo.py index 7be2098aec..7fd4508a19 100644 --- a/clib/mininet_test_topo.py +++ b/clib/mininet_test_topo.py @@ -12,7 +12,7 @@ # pylint: disable=too-many-arguments -from mininet.log import output +from mininet.log import output, warn from mininet.topo import Topo from mininet.node import Controller from mininet.node import CPULimitedHost @@ -24,7 +24,6 @@ # TODO: this should be configurable (e.g for randomization) SWITCH_START_PORT = 5 -HW_OFFSET = 16 # Must be > max(dp_ports) class FaucetIntf(TCIntf): @@ -69,18 +68,40 @@ def __init__(self, name, **params): super().__init__( name=name, reconnectms=8000, **params) + @staticmethod + def _workaround(args): + """Workarounds/hacks for errors resulting from + cmd() calls within Mininet""" + # Workaround: ignore ethtool errors on tap interfaces + # This allows us to use tap tunnels as cables to switch ports, + # for example to test against OvS in a VM. + if (len(args) > 1 and args[0] == 'ethtool -K' and + getattr(args[1], 'name', '').startswith('tap')): + return True + return False + def cmd(self, *args, success=0, **kwargs): - """Switch commands should always succeed, + """Commands typically must succeed for proper switch operation, so we check the exit code of the last command in *args. success: desired exit code (or None to skip check)""" # pylint: disable=arguments-differ cmd_output = super().cmd(*args, **kwargs) exit_code = int(super().cmd('echo $?')) if success is not None and exit_code != success: - raise RuntimeError( - "%s exited with (%d):'%s'" % (args, exit_code, cmd_output)) + msg = "%s exited with (%d):'%s'" % (args, exit_code, cmd_output) + if self._workaround(args): + warn('Ignoring:', msg, '\n') + else: + raise RuntimeError(msg) return cmd_output + def attach(self, intf): + "Attach an interface and set its port" + super().attach(intf) + # This should be done in Mininet, but we do it for now + port = self.ports[intf] + self.cmd('ovs-vsctl set Interface', intf, 'ofport_request=%s' % port) + def start(self, controllers): # Transcluded from Mininet source, since need to insert # controller parameters at switch creation time. diff --git a/docker/runtests.sh b/docker/runtests.sh index 5d17ab2c9c..da4821614e 100755 --- a/docker/runtests.sh +++ b/docker/runtests.sh @@ -9,7 +9,7 @@ set -e # quit on error # allow user to skip parts of docker test # this wrapper script only cares about -n, -u, -i, others passed to test suite. -while getopts "cdijknrsuxoz" o $FAUCET_TESTS; do +while getopts "cdijknrsuxozl" o $FAUCET_TESTS; do case "${o}" in i) # run only integration tests diff --git a/faucet/dp.py b/faucet/dp.py index a5467267c1..35e91df5fd 100644 --- a/faucet/dp.py +++ b/faucet/dp.py @@ -303,6 +303,7 @@ def __init__(self, _id, dp_id, conf): self.stack_root_name = None self.stack_roots_names = None self.stack_route_learning = None + self.stack_root_flood_reflection = None self.acls = {} self.vlans = {} @@ -800,6 +801,9 @@ def resolve_stack_topology(self, dps, meta_dp_state): test_config_condition( path_to_root_len == 0, '%s not connected to stack' % dp) + if self.stack_longest_path_to_root_len() > 2: + self.stack_root_flood_reflection = True + if self.tunnel_acls: self.finalize_tunnel_acls(dps) diff --git a/faucet/valve.py b/faucet/valve.py index 073320cf03..90950dd3a5 100644 --- a/faucet/valve.py +++ b/faucet/valve.py @@ -193,7 +193,7 @@ def dp_init(self, new_dp=None): self.dp.combinatorial_port_flood, self.dp.canonical_port_order, self.dp.stack_ports, self.dp.has_externals, self.dp.shortest_path_to_root, self.dp.shortest_path_port, - self.dp.stack_longest_path_to_root_len, self.dp.is_stack_root, + self.dp.stack_root_flood_reflection, self.dp.is_stack_root, self.dp.is_stack_root_candidate, self.dp.stack.get('graph', None)) else: self.flood_manager = valve_flood.ValveFloodManager( @@ -210,7 +210,7 @@ def dp_init(self, new_dp=None): self.dp.tables['eth_dst'], eth_dst_hairpin_table, self.pipeline, self.dp.timeout, self.dp.learn_jitter, self.dp.learn_ban_timeout, self.dp.cache_update_guard_time, self.dp.idle_dst, self.dp.stack, - self.dp.has_externals) + self.dp.has_externals, self.dp.stack_root_flood_reflection) if any(t in self.dp.tables for t in ('port_acl', 'vlan_acl', 'egress_acl'))\ or self.dp.tunnel_acls: self.acl_manager = valve_acl.ValveAclManager( diff --git a/faucet/valve_flood.py b/faucet/valve_flood.py index dfba8af888..c4c70d7373 100644 --- a/faucet/valve_flood.py +++ b/faucet/valve_flood.py @@ -307,7 +307,7 @@ def __init__(self, logger, flood_table, pipeline, # pylint: disable=too-many-arg combinatorial_port_flood, canonical_port_order, stack_ports, has_externals, dp_shortest_path_to_root, shortest_path_port, - longest_path_to_root_len, is_stack_root, + stack_root_flood_reflection, is_stack_root, is_stack_root_candidate, graph): super(ValveFloodStackManager, self).__init__( logger, flood_table, pipeline, @@ -319,16 +319,13 @@ def __init__(self, logger, flood_table, pipeline, # pylint: disable=too-many-arg self.externals = has_externals self.shortest_path_port = shortest_path_port self.dp_shortest_path_to_root = dp_shortest_path_to_root - self.longest_path_to_root_len = longest_path_to_root_len self.is_stack_root = is_stack_root self.is_stack_root_candidate = is_stack_root_candidate self.graph = graph - stack_size = self.longest_path_to_root_len() - if stack_size is not None and stack_size > 2: - self.logger.info('stack size %u' % stack_size) + self.stack_root_flood_reflection = stack_root_flood_reflection + if self.stack_root_flood_reflection: self._flood_actions_func = self._flood_actions else: - self.logger.info('stack size <= 2, using non root-reflection stacking') self._flood_actions_func = self._flood_actions_size2 self._set_ext_port_flag = [] self._set_nonext_port_flag = [] diff --git a/faucet/valve_host.py b/faucet/valve_host.py index 6d2927ab4b..01ca9a236b 100644 --- a/faucet/valve_host.py +++ b/faucet/valve_host.py @@ -27,7 +27,7 @@ class ValveHostManager(ValveManagerBase): def __init__(self, logger, ports, vlans, eth_src_table, eth_dst_table, eth_dst_hairpin_table, pipeline, learn_timeout, learn_jitter, learn_ban_timeout, cache_update_guard_time, idle_dst, stack, - has_externals): + has_externals, stack_root_flood_reflection): self.logger = logger self.ports = ports self.vlans = vlans @@ -45,6 +45,7 @@ def __init__(self, logger, ports, vlans, eth_src_table, eth_dst_table, self.idle_dst = idle_dst self.stack = stack self.has_externals = has_externals + self.stack_root_flood_reflection = stack_root_flood_reflection if self.eth_dst_hairpin_table: self.output_table = self.eth_dst_hairpin_table @@ -301,7 +302,8 @@ def learn_host_on_vlan_ports(self, now, port, vlan, eth_src, # stacks of size > 2 will have an unknown MAC flooded towards the root, # and flooded down again. If we learned the MAC on a local port and # heard the reflected flooded copy, discard the reflection. - local_stack_learn = (port.stack and not cache_port.stack) + local_stack_learn = ( + self.stack_root_flood_reflection and port.stack and not cache_port.stack) guard_time = self.cache_update_guard_time if cache_port == port or same_lag or local_stack_learn: # aggressively re-learn on LAGs, and prefer recently learned diff --git a/tests/integration/mininet_tests.py b/tests/integration/mininet_tests.py index 3025359e76..523d955b65 100644 --- a/tests/integration/mininet_tests.py +++ b/tests/integration/mininet_tests.py @@ -2,6 +2,7 @@ """Mininet tests for FAUCET.""" +# pylint: disable=too-many-lines # pylint: disable=missing-docstring # pylint: disable=too-many-arguments # pylint: disable=unbalanced-tuple-unpacking @@ -122,9 +123,12 @@ class FaucetUntaggedTest(FaucetTest): description: "untagged" """ + # pylint: disable=invalid-name CONFIG = CONFIG_BOILER_UNTAGGED - def setUp(self): # pylint: disable=invalid-name + EVENT_LOGGER_TIMEOUT = 120 # Timeout for event logger process + + def setUp(self): # pylint: disable=invalid-name super(FaucetUntaggedTest, self).setUp() self.topo = self.topo_class( self.OVS_TYPE, self.ports_sock, self._test_name(), [self.dpid], @@ -157,8 +161,11 @@ def test_untagged(self): event_log = os.path.join(self.tmpdir, 'event.log') controller = self._get_controller() sock = self.env['faucet']['FAUCET_EVENT_SOCK'] + # Relying on a timeout seems a bit brittle; + # as an alternative we might possibly use something like + # `with popen(cmd...) as proc` to clean up on exceptions controller.cmd(mininet_test_util.timeout_cmd( - 'nc -U %s > %s &' % (sock, event_log), 120)) + 'nc -U %s > %s &' % (sock, event_log), self.EVENT_LOGGER_TIMEOUT)) self.ping_all_when_learned() self.flap_all_switch_ports() self.verify_traveling_dhcp_mac() @@ -7360,20 +7367,25 @@ def test_untagged(self): for int_host in int_hosts: # All internal hosts can reach other internal hosts. for other_int_host in int_hosts - {int_host}: - self.verify_broadcast(hosts=(int_host, other_int_host), broadcast_expected=True) - self.verify_unicast(hosts=(int_host, other_int_host), unicast_expected=True) + # force ARP to test broadcast. + int_host.cmd( + 'arp -d %s' % other_int_host.IP()) + self.one_ipv4_ping( + int_host, other_int_host.IP(), require_host_learned=False) for ext_host in ext_hosts: # All external hosts cannot flood to each other for other_ext_host in ext_hosts - {ext_host}: - self.verify_broadcast(hosts=(ext_host, other_ext_host), broadcast_expected=False) + self.verify_broadcast( + hosts=(ext_host, other_ext_host), broadcast_expected=False) remote_ext_hosts = ext_hosts - set(root_ext_hosts) # int host should never be broadcast to an ext host that is not on the root. for local_int_hosts, _ in dp_hosts.values(): local_int_host = list(local_int_hosts)[0] for remote_ext_host in remote_ext_hosts: - self.verify_broadcast(hosts=(local_int_host, remote_ext_host), broadcast_expected=False) + self.verify_broadcast( + hosts=(local_int_host, remote_ext_host), broadcast_expected=False) class FaucetGroupStackStringOfDPUntaggedTest(FaucetStackStringOfDPUntaggedTest): @@ -8100,8 +8112,24 @@ def bad_port(self): return (max_port + offset) & mask def bad_match(self): - """Match with bad input port""" - return {'match': {'in_port': self.bad_port()}} + """Return a bad match field""" + matches = ( + # Bad input port + {'in_port': self.bad_port()}, + # IPv4 (broadcast) src with bad ('reserved') ethertype + {'nw_src': '255.255.255.255', 'dl_type': 0xFFFF}, + # IPv4 with IPv6 ethertype: + {'nw_src': '1.2.3.4', 'dl_type': 0x86DD}, + # IPv4 address as IPv6 dst + {'ipv6_dst': '1.2.3.4', 'dl_type': 0x86DD}, + # IPv6 dst with Bad/reserved ip_proto + {'ipv6_dst': '2001::aaaa:bbbb:cccc:1111', 'ip_proto': 255}, + # Destination port but no transport protocol + {'tp_dst': 80}, + # ARP opcode on non-ARP packetx + {'arp_op': 0x3, 'dl_type': 0x1234}) + match = random.sample(matches, 1)[0] + return {'match': match} def bad_actions(self, count=1): """Return a questionable actions parameter""" @@ -8148,3 +8176,50 @@ def test_untagged(self, count=10): error('sending bad flow_mod', flow_mod, '\n') self.send_flow_mod(flow_mod) self.ping_all_when_learned() + + +class FaucetUntaggedMorePortsBase(FaucetUntaggedTest): + """Base class for untagged test with more ports""" + + # pylint: disable=invalid-name + N_UNTAGGED = 16 # Maximum number of ports to test + EVENT_LOGGER_TIMEOUT = 180 # Timeout for event logger process + + # Config lines for additional ports + CONFIG_EXTRA_PORT = """ + {port}: + native_vlan: 100""" + "\n" + + def _init_faucet_config(self): # pylint: disable=invalid-name + """Extend config with more ports if needed""" + self.assertTrue(self.CONFIG.endswith(CONFIG_BOILER_UNTAGGED)) + # We know how to extend the config for more ports + base_port_count = len(re.findall('port', CONFIG_BOILER_UNTAGGED)) + ports = self.topo.dpid_ports(self.dpid) + for port in ports[base_port_count:]: + self.CONFIG += self.CONFIG_EXTRA_PORT.format(port=port) + super()._init_faucet_config() + + def setUp(self): + """Make sure N_UNTAGGED doesn't exceed hw port count""" + if self.config and self.config.get('hw_switch', False): + self.N_UNTAGGED = min(len(self.config['dp_ports']), + self.N_UNTAGGED) + error('(%d ports) ' % self.N_UNTAGGED) + super().setUp() + + +class FaucetUntagged32PortTest(FaucetUntaggedMorePortsBase): + """Untagged test with up to 32 ports""" + + # pylint: disable=invalid-name + N_UNTAGGED = 32 # Maximum number of ports to test + + +@unittest.skip('slow and potentially unreliable on travis') +class FaucetUntagged48PortTest(FaucetUntaggedMorePortsBase): + """Untagged test with up to 48 ports""" + + # pylint: disable=invalid-name + N_UNTAGGED = 48 # Maximum number of ports to test + EVENT_LOGGER_TIMEOUT = 360 # Timeout for event logger process