diff --git a/.gitignore b/.gitignore index 48da8416f..46d206c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc profiles/*.pyc *~ +.*.swp diff --git a/src/python/oftest/dataplane.py b/src/python/oftest/dataplane.py index 0cdbaa886..ef08e30fe 100644 --- a/src/python/oftest/dataplane.py +++ b/src/python/oftest/dataplane.py @@ -32,6 +32,8 @@ else: import pcap +MATCH_VERBOSE=False + def match_exp_pkt(exp_pkt, pkt): """ Compare the string value of pkt with the string value of exp_pkt, @@ -43,6 +45,13 @@ def match_exp_pkt(exp_pkt, pkt): p = str(pkt) if len(e) < 60: p = p[:len(e)] + if MATCH_VERBOSE and e != p: + i = 0 + for i in range(0, min(len(e), len(p))): + if e[i] != p[i]: + break + logging.info("Ignoring packet that doesn't match on byte %d: expected is \'%s\' got \'%s\'" + % (i, hex(ord(e[i])), hex(ord(p[i])))) return e == p diff --git a/src/python/oftest/oft12/testutils.py b/src/python/oftest/oft12/testutils.py index d6dbdf0a4..00b099a15 100644 --- a/src/python/oftest/oft12/testutils.py +++ b/src/python/oftest/oft12/testutils.py @@ -238,6 +238,7 @@ def simple_ipv6_packet(pktlen=100, ip_src='fe80::2420:52ff:fe8f:5189', ip_dst='fe80::2420:52ff:fe8f:5190', ip_tos=0, + ip_ttl=64, tcp_sport=0, tcp_dport=0, EH = False, @@ -265,11 +266,11 @@ def simple_ipv6_packet(pktlen=100, if (dl_vlan_enable): pkt = Ether(dst=dl_dst, src=dl_src)/ \ Dot1Q(prio=dl_vlan_pcp, id=dl_vlan_cfi, vlan=dl_vlan)/ \ - IPv6(src=ip_src, dst=ip_dst) + IPv6(src=ip_src, dst=ip_dst, hlim=ip_ttl) else: pkt = Ether(dst=dl_dst, src=dl_src)/ \ - IPv6(src=ip_src, dst=ip_dst) + IPv6(src=ip_src, dst=ip_dst, hlim=ip_ttl) # Add IPv6 Extension Headers if EH: diff --git a/tests-ofdpa-2.0/basic.py b/tests-ofdpa-2.0/basic.py new file mode 100644 index 000000000..c8b00b1ff --- /dev/null +++ b/tests-ofdpa-2.0/basic.py @@ -0,0 +1,598 @@ +# Distributed under the OpenFlow Software License (see LICENSE) +# Copyright (c) 2010 The Board of Trustees of The Leland Stanford Junior University +# Copyright (c) 2012, 2013 Big Switch Networks, Inc. +# Copyright (c) 2012, 2013 CPqD +# Copyright (c) 2012, 2013 Ericsson +""" +Basic test cases + +Test cases in other modules depend on this functionality. +""" + +import logging + +from oftest import config +import oftest.base_tests as base_tests +import ofp +import ofdpa_utils + +from oftest.testutils import * + +@group('smoke') +class Echo(base_tests.SimpleProtocol): + """ + Test echo response with no data + """ + def runTest(self): + request = ofp.message.echo_request() + response, pkt = self.controller.transact(request) + self.assertTrue(response is not None, + "Did not get echo reply") + self.assertEqual(response.type, ofp.OFPT_ECHO_REPLY, + 'response is not echo_reply') + self.assertEqual(request.xid, response.xid, + 'response xid != request xid') + self.assertEqual(len(response.data), 0, 'response data non-empty') + +class EchoWithData(base_tests.SimpleProtocol): + """ + Test echo response with short string data + """ + def runTest(self): + data = 'OpenFlow Will Rule The World' + request = ofp.message.echo_request(data=data) + response, _ = self.controller.transact(request) + self.assertTrue(response is not None, + "Did not get echo reply") + self.assertEqual(response.type, ofp.OFPT_ECHO_REPLY, + 'response is not echo_reply') + self.assertEqual(request.xid, response.xid, + 'response xid != request xid') + self.assertEqual(request.data, response.data, + 'response data != request data') + +class FeaturesRequest(base_tests.SimpleProtocol): + """ + Test features_request to make sure we get a response + + Does NOT test the contents; just that we get a response + """ + def runTest(self): + request = ofp.message.features_request() + response,_ = self.controller.transact(request) + self.assertTrue(response is not None, + 'Did not get features reply') + +class DefaultDrop(base_tests.SimpleDataPlane): + """ + Check that an empty flowtable results in drops + """ + def runTest(self): + in_port, = openflow_ports(1) + delete_all_flows(self.controller) + + pkt = str(simple_tcp_packet()) + self.dataplane.send(in_port, pkt) + verify_no_packet_in(self, pkt, None) + verify_packets(self, pkt, []) + +class OutputExact(base_tests.SimpleDataPlane): + """ + Test output function for an exact-match flow + + For each port A, adds a flow directing matching packets to that port. + Then, for all other ports B != A, verifies that sending a matching packet + to B results in an output to A. + """ + def runTest(self): + ports = sorted(config["port_map"].keys()) + + delete_all_flows(self.controller) + + parsed_pkt = simple_tcp_packet() + pkt = str(parsed_pkt) + match = packet_to_flow_match(self, parsed_pkt) + + for out_port in ports: + request = ofp.message.flow_add( + table_id=test_param_get("table", 0), + cookie=42, + match=match, + instructions=[ + ofp.instruction.apply_actions( + actions=[ + ofp.action.output( + port=out_port, + max_len=ofp.OFPCML_NO_BUFFER)])], + buffer_id=ofp.OFP_NO_BUFFER, + priority=1000) + + logging.info("Inserting flow sending matching packets to port %d", out_port) + self.controller.message_send(request) + do_barrier(self.controller) + + for in_port in ports: + if in_port == out_port: + continue + logging.info("OutputExact test, ports %d to %d", in_port, out_port) + self.dataplane.send(in_port, pkt) + verify_packets(self, pkt, [out_port]) + +class OutputWildcard(base_tests.SimpleDataPlane): + """ + Test output function for a match-all (but not table-miss) flow + + For each port A, adds a flow directing all packets to that port. + Then, for all other ports B != A, verifies that sending a packet + to B results in an output to A. + """ + def runTest(self): + ports = sorted(config["port_map"].keys()) + + delete_all_flows(self.controller) + + pkt = str(simple_tcp_packet()) + + for out_port in ports: + request = ofp.message.flow_add( + table_id=test_param_get("table", 0), + cookie=42, + instructions=[ + ofp.instruction.apply_actions( + actions=[ + ofp.action.output( + port=out_port, + max_len=ofp.OFPCML_NO_BUFFER)])], + buffer_id=ofp.OFP_NO_BUFFER, + priority=1000) + + logging.info("Inserting flow sending all packets to port %d", out_port) + self.controller.message_send(request) + do_barrier(self.controller) + + for in_port in ports: + if in_port == out_port: + continue + logging.info("OutputWildcard test, ports %d to %d", in_port, out_port) + self.dataplane.send(in_port, pkt) + verify_packets(self, pkt, [out_port]) + +class PacketInExact(base_tests.SimpleDataPlane): + """ + Test packet in function for an exact-match flow + + Send a packet to each dataplane port and verify that a packet + in message is received from the controller for each + """ + def runTest(self): + delete_all_flows(self.controller) + + # required for OF-DPA to not drop packets + ofdpa_utils.installDefaultVlan(self.controller) + + parsed_pkt = simple_tcp_packet() + pkt = str(parsed_pkt) + + # NOTE: interally the switch adds a VLAN so the match needs to be with an explicit VLAN + parsed_vlan_pkt = simple_tcp_packet(dl_vlan_enable=True, + vlan_vid=ofdpa_utils.DEFAULT_VLAN, + vlan_pcp=0, + pktlen=104) # 4 less than we started with, because the way simple_tcp calc's length + match = packet_to_flow_match(self, parsed_vlan_pkt) + vlan_pkt = str(parsed_vlan_pkt) + + request = ofp.message.flow_add( + table_id=ofdpa_utils.ACL_TABLE.table_id, + cookie=42, + match=match, + instructions=[ + ofp.instruction.apply_actions( + actions=[ + ofp.action.output( + port=ofp.OFPP_CONTROLLER, + max_len=ofp.OFPCML_NO_BUFFER)])], + buffer_id=ofp.OFP_NO_BUFFER, + priority=1000) + + logging.info("Inserting flow sending matching packets to controller") + self.controller.message_send(request) + do_barrier(self.controller) + + for of_port in config["port_map"].keys(): + logging.info("PacketInExact test, port %d", of_port) + self.dataplane.send(of_port, pkt) + verify_packet_in(self, vlan_pkt, of_port, ofp.OFPR_ACTION) + verify_packets(self, pkt, []) + +class PacketInWildcard(base_tests.SimpleDataPlane): + # NOTE: interally the switch adds a VLAN so the match needs to be with an explicit VLAN + """ + Test packet in function for a match-all flow + + Send a packet to each dataplane port and verify that a packet + in message is received from the controller for each + """ + def runTest(self): + delete_all_flows(self.controller) + + # required for OF-DPA to not drop packets + ofdpa_utils.installDefaultVlan(self.controller) + + pkt = str(simple_tcp_packet()) + + # NOTE: interally the switch adds a VLAN so the match needs to be with an explicit VLAN + parsed_vlan_pkt = simple_tcp_packet(dl_vlan_enable=True, + vlan_vid=ofdpa_utils.DEFAULT_VLAN, + vlan_pcp=0, + pktlen=104) # 4 less than we started with, because the way simple_tcp calc's length + vlan_pkt = str(parsed_vlan_pkt) + + + request = ofp.message.flow_add( + table_id=ofdpa_utils.ACL_TABLE.table_id, + cookie=42, + instructions=[ + ofp.instruction.apply_actions( + actions=[ + ofp.action.output( + port=ofp.OFPP_CONTROLLER, + max_len=ofp.OFPCML_NO_BUFFER)])], + buffer_id=ofp.OFP_NO_BUFFER, + priority=1000) + + logging.info("Inserting flow sending all packets to controller") + self.controller.message_send(request) + do_barrier(self.controller) + + for of_port in config["port_map"].keys(): + logging.info("PacketInWildcard test, port %d", of_port) + self.dataplane.send(of_port, pkt) + verify_packet_in(self, vlan_pkt, of_port, ofp.OFPR_ACTION) + verify_packets(self, pkt, []) + +class PacketInMiss(base_tests.SimpleDataPlane): + """ + Test packet in function for a table-miss flow + + Send a packet to each dataplane port and verify that a packet + in message is received from the controller for each + """ + def runTest(self): + delete_all_flows(self.controller) + + # required for OF-DPA to not drop packets + ofdpa_utils.installDefaultVlan(self.controller) + + parsed_pkt = simple_tcp_packet() + pkt = str(parsed_pkt) + + # NOTE: interally the switch adds a VLAN so the match needs to be with an explicit VLAN + parsed_vlan_pkt = simple_tcp_packet(dl_vlan_enable=True, + vlan_vid=ofdpa_utils.DEFAULT_VLAN, + vlan_pcp=0, + pktlen=104) # 4 less than we started with, because the way simple_tcp calc's length + vlan_pkt = str(parsed_vlan_pkt) + + request = ofp.message.flow_add( + table_id=ofdpa_utils.ACL_TABLE.table_id, + cookie=42, + instructions=[ + ofp.instruction.apply_actions( + actions=[ + ofp.action.output( + port=ofp.OFPP_CONTROLLER, + max_len=ofp.OFPCML_NO_BUFFER)])], + buffer_id=ofp.OFP_NO_BUFFER, + priority=0) + + logging.info("Inserting table-miss flow sending all packets to controller") + self.controller.message_send(request) + do_barrier(self.controller) + + for of_port in config["port_map"].keys(): + logging.info("PacketInMiss test, port %d", of_port) + self.dataplane.send(of_port, pkt) + verify_packet_in(self, vlan_pkt, of_port, ofp.OFPR_NO_MATCH) + verify_packets(self, pkt, []) + +class PacketOut(base_tests.SimpleDataPlane): + """ + Test packet out function + + Send packet out message to controller for each dataplane port and + verify the packet appears on the appropriate dataplane port + """ + def runTest(self): + pkt = str(simple_tcp_packet()) + + for of_port in config["port_map"].keys(): + msg = ofp.message.packet_out( + in_port=ofp.OFPP_CONTROLLER, + actions=[ofp.action.output(port=of_port)], + buffer_id=ofp.OFP_NO_BUFFER, + data=pkt) + + logging.info("PacketOut test, port %d", of_port) + self.controller.message_send(msg) + verify_packets(self, pkt, [of_port]) + +class FlowRemoveAll(base_tests.SimpleProtocol): + """ + Remove all flows; required for almost all tests + + Add a bunch of flows, remove them, and then make sure there are no flows left + This is an intentionally naive test to see if the baseline functionality works + and should be a precondition to any more complicated deletion test (e.g., + delete_strict vs. delete) + """ + def runTest(self): + for i in range(1,5): + logging.debug("Adding flow %d", i) + request = ofp.message.flow_add( + buffer_id=ofp.OFP_NO_BUFFER, + priority=i*1000) + self.controller.message_send(request) + do_barrier(self.controller) + + delete_all_flows(self.controller) + + logging.info("Sending flow stats request") + stats = get_flow_stats(self, ofp.match()) + self.assertEqual(len(stats), 0, "Expected empty flow stats reply") + + +## Multipart messages + +class DescStats(base_tests.SimpleProtocol): + """ + Switch description multipart transaction + + Only verifies we get a single reply. + """ + def runTest(self): + request = ofp.message.desc_stats_request() + logging.info("Sending desc stats request") + response, _ = self.controller.transact(request) + self.assertTrue(response != None, "No response to desc stats request") + logging.info(response.show()) + self.assertEquals(response.flags, 0, "Unexpected bit set in desc stats reply flags") + +class FlowStats(base_tests.SimpleProtocol): + """ + Flow stats multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + logging.info("Sending flow stats request") + stats = get_flow_stats(self, ofp.match()) + logging.info("Received %d flow stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class AggregateStats(base_tests.SimpleProtocol): + """ + Aggregate flow stats multipart transaction + + Only verifies we get a single reply. + """ + def runTest(self): + request = ofp.message.aggregate_stats_request( + table_id=ofp.OFPTT_ALL, + out_port=ofp.OFPP_ANY, + out_group=ofp.OFPG_ANY, + cookie=0, + cookie_mask=0) + logging.info("Sending aggregate flow stats request") + response, _ = self.controller.transact(request) + self.assertTrue(response != None, "No response to aggregate stats request") + logging.info(response.show()) + self.assertEquals(response.flags, 0, "Unexpected bit set in aggregate stats reply flags") + +class TableStats(base_tests.SimpleProtocol): + """ + Table stats multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + logging.info("Sending table stats request") + stats = get_stats(self, ofp.message.table_stats_request()) + logging.info("Received %d table stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class PortStats(base_tests.SimpleProtocol): + """ + Port stats multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + request = ofp.message.port_stats_request(port_no=ofp.OFPP_ANY) + logging.info("Sending port stats request") + stats = get_stats(self, request) + logging.info("Received %d port stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class QueueStats(base_tests.SimpleProtocol): + """ + Queue stats multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + request = ofp.message.queue_stats_request(port_no=ofp.OFPP_ANY, + queue_id=ofp.OFPQ_ALL) + logging.info("Sending queue stats request") + stats = get_stats(self, request) + logging.info("Received %d queue stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class GroupStats(base_tests.SimpleProtocol): + """ + Group stats multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + request = ofp.message.group_stats_request(group_id=ofp.OFPG_ALL) + logging.info("Sending group stats request") + stats = get_stats(self, request) + logging.info("Received %d group stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class GroupDescStats(base_tests.SimpleProtocol): + """ + Group description multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + request = ofp.message.group_desc_stats_request() + logging.info("Sending group desc stats request") + stats = get_stats(self, request) + logging.info("Received %d group desc stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class GroupFeaturesStats(base_tests.SimpleProtocol): + """ + Group features multipart transaction + + Only verifies we get a single reply. + """ + def runTest(self): + request = ofp.message.group_features_stats_request() + logging.info("Sending group features stats request") + response, _ = self.controller.transact(request) + self.assertTrue(response != None, "No response to group features stats request") + logging.info(response.show()) + self.assertEquals(response.flags, 0, "Unexpected bit set in group features stats reply flags") + +class MeterStats(base_tests.SimpleProtocol): + """ + Meter stats multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + request = ofp.message.meter_stats_request(meter_id=ofp.OFPM_ALL) + logging.info("Sending meter stats request") + stats = get_stats(self, request) + logging.info("Received %d meter stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class MeterConfigStats(base_tests.SimpleProtocol): + """ + Meter config multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + request = ofp.message.meter_config_stats_request(meter_id=ofp.OFPM_ALL) + logging.info("Sending meter config stats request") + stats = get_stats(self, request) + logging.info("Received %d meter config stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class MeterFeaturesStats(base_tests.SimpleProtocol): + """ + Meter features multipart transaction + + Only verifies we get a single reply. + """ + def runTest(self): + request = ofp.message.meter_features_stats_request() + logging.info("Sending meter features stats request") + response, _ = self.controller.transact(request) + self.assertTrue(response != None, "No response to meter features stats request") + logging.info(response.show()) + self.assertEquals(response.flags, 0, "Unexpected bit set in meter features stats reply flags") + +@disabled # pyloxi does not yet support table features +class TableFeaturesStats(base_tests.SimpleProtocol): + """ + Table features multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + logging.info("Sending table features stats request") + stats = get_stats(self, ofp.message.table_features_stats_request()) + logging.info("Received %d table features stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class PortDescStats(base_tests.SimpleProtocol): + """ + Port description multipart transaction + + Only verifies we get a reply. + """ + def runTest(self): + logging.info("Sending port desc stats request") + stats = get_stats(self, ofp.message.port_desc_stats_request()) + logging.info("Received %d port desc stats entries", len(stats)) + for entry in stats: + logging.info(entry.show()) + +class PortConfigMod(base_tests.SimpleProtocol): + """ + Modify a bit in port config and verify changed + + Get the switch configuration, modify the port configuration + and write it back; get the config again and verify changed. + Then set it back to the way it was. + """ + + def runTest(self): + logging.info("Running " + str(self)) + for of_port, _ in config["port_map"].items(): # Grab first port + break + + (_, config1, _) = \ + port_config_get(self.controller, of_port) + self.assertTrue(config is not None, "Did not get port config") + + logging.debug("OFPPC_NO_PACKET_IN bit port " + str(of_port) + " is now " + + str(config1 & ofp.OFPPC_NO_PACKET_IN)) + + rv = port_config_set(self.controller, of_port, + config1 ^ ofp.OFPPC_NO_PACKET_IN, + ofp.OFPPC_NO_PACKET_IN) + self.assertTrue(rv != -1, "Error sending port mod") + + # Verify change took place with same feature request + (_, config2, _) = port_config_get(self.controller, of_port) + self.assertTrue(config2 is not None, "Did not get port config2") + logging.debug("OFPPC_NO_PACKET_IN bit port " + str(of_port) + " is now " + + str(config2 & ofp.OFPPC_NO_PACKET_IN)) + self.assertTrue(config2 & ofp.OFPPC_NO_PACKET_IN != + config1 & ofp.OFPPC_NO_PACKET_IN, + "Bit change did not take") + # Set it back + rv = port_config_set(self.controller, of_port, config1, + ofp.OFPPC_NO_PACKET_IN) + self.assertTrue(rv != -1, "Error sending port mod") + +class AsyncConfigGet(base_tests.SimpleProtocol): + """ + Verify initial async config + + Other tests rely on connections starting with these values. + """ + + def runTest(self): + logging.info("Sending get async config request") + response, _ = self.controller.transact(ofp.message.async_get_request()) + self.assertTrue(response != None, "No response to get async config request") + logging.info(response.show()) + self.assertEquals(response.packet_in_mask_equal_master & 0x07, 0x07) + self.assertEquals(response.port_status_mask_equal_master & 0x07, 0x07) + self.assertEquals(response.flow_removed_mask_equal_master & 0x0f, 0x0f) diff --git a/tests-ofdpa-2.0/ipv6_fwd.py b/tests-ofdpa-2.0/ipv6_fwd.py new file mode 100644 index 000000000..2212d8833 --- /dev/null +++ b/tests-ofdpa-2.0/ipv6_fwd.py @@ -0,0 +1,368 @@ +# Copyright (c) 2015 Big Switch Networks, Inc. +""" +Testing IPv6 forwarding + +Test cases in other modules depend on this functionality. +""" + +import logging +import pdb + +from oftest import config +import oftest.base_tests as base_tests +import ofp +import ofdpa_utils +import oftest.dataplane +from oftest.parse import parse_ipv6 + +from oftest.testutils import * +import oftest.oft12.testutils as testutils12 + +def macAddrToHexList(mac): + """ Takes '00:11:22:33:44:ff' and returns [ 0x00, 0x11, 0x22, 0x33, 0x44, 0xff] """ + return map(int,mac.split(':'),[16]*6) + + + +class L3Iface(object): + def __init__(self, mac,vlan,port, dst_mac, dst_ip): + self.mac = mac # this is the mac of the router interface + self.port = port # this is the port of the router + self.vlan = vlan + self.dst_mac = dst_mac + self.dst_ip = dst_ip + + +class IPv6_Untagged_Unicast(base_tests.SimpleDataPlane): + """ + Test routing function for an IPv6 IP packet + + Need to insert rules into: + 1) A group action in the group table + 2) Vlan table to map packet to a vlan + 3) Mac Term table to send router's mac packets to the routing table + 4) the actual routing rule in the unicast routing table + + Then test from all ports to all ports + + """ + def create_interface(self, iface, untagged=True): + iface.l2_group_id = ofdpa_utils.makeGroupID("L2 Interface",(iface.vlan<<16) + iface.port) # bits 27:16 of the local_id are the vlan (!?) + iface.l3_group_id = ofdpa_utils.makeGroupID("L3 Unicast",iface.port) + actions = [ ofp.action.output(iface.port) ] + if untagged: + actions.append(ofp.action.pop_vlan()) + + self.send_log_check("Creating IPv6 L2 interface group for port %d" % iface.port, ofp.message.group_add( + group_type=ofp.OFPGT_ALL, + group_id=iface.l2_group_id, + buckets=[ + ofp.bucket( actions=actions) + ])) + + # Insert a rule in the MAC termination table to send to Unicast router table + # Must match on dst mac and ethertype; other fields are optional + macterm_match = ofp.match([ + ofp.oxm.eth_dst(macAddrToHexList(iface.mac)), + ofp.oxm.eth_type(0x86dd) # ethertype = IPv6 + ]) + + self.send_log_check( "Inserting IPv6 MacTermination rule", ofp.message.flow_add( + table_id = ofdpa_utils.MACTERM_TABLE.table_id, + cookie=55, + match=macterm_match, + instructions=[ + ofp.instruction.goto_table(ofdpa_utils.UNICAST_ROUTING_TABLE.table_id) + ], + buffer_id=ofp.OFP_NO_BUFFER, + priority=1000)) + + # Create the L3 unicast group entry + self.send_log_check("Creating IPv6 Unicast group for %d" % iface.port, ofp.message.group_add( + group_type=ofp.OFPGT_ALL, + group_id=iface.l3_group_id, + buckets=[ + ofp.bucket( + actions=[ + ofp.action.group(iface.l2_group_id), # chain to this interface's L2 group, per OF-DPA spec + ofp.action.set_field( ofp.oxm.eth_dst( macAddrToHexList(iface.dst_mac))), + ofp.action.set_field( ofp.oxm.eth_src( macAddrToHexList(iface.mac))), + ofp.action.set_field( ofp.oxm.vlan_vid( iface.vlan)), + ]) + ])) + + # Store the match for the unicast match below + unicast_match = ofp.match([ + ofp.oxm.eth_type(0x86dd), # ethertype = IPv6 + ofp.oxm.ipv6_dst(parse_ipv6(iface.dst_ip)), + ]) + + # OF-DPA requires vlans to function + if untagged: + ofdpa_utils.installDefaultVlan(self.controller, iface.vlan, iface.port) + else: + ofdpa_utils.enableVlanOnPort(self.controller, iface.vlan, iface.port) + + + self.send_log_check("Modifying IPv6 route rule to output to port %d" % iface.port, ofp.message.flow_modify( + table_id = ofdpa_utils.UNICAST_ROUTING_TABLE.table_id, + cookie=66, + match=unicast_match, + instructions=[ + ofp.instruction.goto_table(ofdpa_utils.ACL_TABLE.table_id), + # set the l3_group_id to correspond to out_port's buckets + ofp.instruction.write_actions([ + ofp.action.group(ofdpa_utils.makeGroupID("L3 Unicast",iface.port))]) + ], + buffer_id=ofp.OFP_NO_BUFFER, + priority=1000)) + + + def send_log_check(self, log_msg, of_msg): + logging.info(log_msg) + self.controller.message_send(of_msg) + do_barrier(self.controller) + verify_no_errors(self.controller) + + def runTest(self): + ports = sorted(config["port_map"].keys()) + # FIXME test to make sure we have at least 6 ports + + delete_all_flows(self.controller) + delete_all_groups(self.controller) # FIXME this fails to recursively delete groups + do_barrier(self.controller) + + + ifaces = dict() + + # FIXME hack around potentially missing/non-contiguous ports + if 1 in ports: + # mac,vlan,port, dst_mac, dst_ip + ifaces[L3Iface('00:11:11:11:11:11', 1, 1, '00:11:11:11:11:ff', 'fe80::1111')] = 1 + + if 2 in ports: + ifaces[L3Iface('00:22:22:22:22:22', 2, 2, '00:22:22:22:22:ff', 'fe80::2222')] = 2 + + if 3 in ports: + ifaces[L3Iface('00:33:33:33:33:33', 3, 3, '00:33:33:33:33:ff', 'fe80::3333')] = 3 + + if 4 in ports: + ifaces[L3Iface('00:44:44:44:44:44', 4, 4, '00:44:44:44:44:ff', 'fe80::4444')] = 4 + + if 5 in ports: + ifaces[L3Iface('00:55:55:55:55:55', 5, 5, '00:55:55:55:55:ff', 'fe80::5555')] = 5 + + if 6 in ports: + ifaces[L3Iface('00:66:66:66:66:66', 6, 6, '00:66:66:66:66:ff', 'fe80::6666')] = 6 + + for iface in sorted(ifaces): + self.create_interface(iface) + + # Store the match for the unicast match below + # test sending from every inteface to every other interface + for in_iface in sorted(ifaces): + for out_iface in sorted(ifaces): + if in_iface == out_iface: + continue + # Generate the test and expected packets + parsed_pkt = testutils12.simple_ipv6_packet( + ip_src = in_iface.dst_ip, + ip_dst = out_iface.dst_ip, + dl_dst = in_iface.mac) + pkt = str(parsed_pkt) + + parsed_expected_pkt = testutils12.simple_ipv6_packet( + dl_src = out_iface.mac, + dl_dst = out_iface.dst_mac, + ip_src = in_iface.dst_ip, + ip_dst = out_iface.dst_ip, + ip_ttl = 63, +# dl_vlan_enable=True, +# dl_vlan=ofdpa_utils.DEFAULT_VLAN, +# vlan_pcp=0, +# pktlen=104 # 4 less than we started with, because the way simple_tcp calc's length + ) + expected_pkt = str(parsed_expected_pkt) + logging.info("IPv6_Untagged_Unicast test, sending iface %d to %d", ifaces[in_iface], ifaces[out_iface]) + #logging.info("Expecting packet: %s" % parsed_expected_pkt.show2()) + self.dataplane.send(in_iface.port, pkt) + oftest.dataplane.MATCH_VERBOSE=True + verify_packets(self, expected_pkt, [out_iface.port]) + + + +class IPv6_Tagged_Unicast(IPv6_Untagged_Unicast): + def runTest(self): + ports = sorted(config["port_map"].keys()) + # FIXME test to make sure we have at least 6 ports + + delete_all_flows(self.controller) + delete_all_groups(self.controller) # FIXME this fails to recursively delete groups + do_barrier(self.controller) + + + ifaces = dict() + + # FIXME hack around potentially missing/non-contiguous ports + if 1 in ports: + # mac,vlan,port, dst_mac, dst_ip + ifaces[L3Iface('00:11:11:11:11:11', 1, 1, '00:11:11:11:11:ff', 'fe80::1111')] = 1 + + if 2 in ports: + ifaces[L3Iface('00:22:22:22:22:22', 2, 2, '00:22:22:22:22:ff', 'fe80::2222')] = 2 + + if 3 in ports: + ifaces[L3Iface('00:33:33:33:33:33', 3, 3, '00:33:33:33:33:ff', 'fe80::3333')] = 3 + + if 4 in ports: + ifaces[L3Iface('00:44:44:44:44:44', 4, 4, '00:44:44:44:44:ff', 'fe80::4444')] = 4 + + if 5 in ports: + ifaces[L3Iface('00:55:55:55:55:55', 5, 5, '00:55:55:55:55:ff', 'fe80::5555')] = 5 + + if 6 in ports: + ifaces[L3Iface('00:66:66:66:66:66', 6, 6, '00:66:66:66:66:ff', 'fe80::6666')] = 6 + + for iface in ifaces: + self.create_interface(iface, untagged=False) + + # Store the match for the unicast match below + # test sending from every inteface to every other interface + for in_iface in sorted(ifaces): + for out_iface in sorted(ifaces): + if in_iface == out_iface: + continue + # Generate the test and expected packets + parsed_pkt = testutils12.simple_ipv6_packet( + ip_src = in_iface.dst_ip, + ip_dst = out_iface.dst_ip, + dl_vlan_enable=True, + dl_vlan = in_iface.vlan, + dl_dst = in_iface.mac) + pkt = str(parsed_pkt) + + parsed_expected_pkt = testutils12.simple_ipv6_packet( + dl_src = out_iface.mac, + dl_dst = out_iface.dst_mac, + ip_src = in_iface.dst_ip, + ip_dst = out_iface.dst_ip, + ip_ttl = 63, + dl_vlan_enable=True, + dl_vlan=out_iface.vlan +# pktlen=104 # 4 less than we started with, because the way simple_tcp calc's length + ) + expected_pkt = str(parsed_expected_pkt) + logging.info("IPv6_Tagged_Unicast test, sending iface %d to %d", ifaces[in_iface], ifaces[out_iface]) + #logging.info("Expecting packet: %s" % parsed_expected_pkt.show2()) + self.dataplane.send(in_iface.port, pkt) + oftest.dataplane.MATCH_VERBOSE=True + verify_packets(self, expected_pkt, [out_iface.port]) + + +class IPv6_Untagged_ECMP(IPv6_Untagged_Unicast): + """ + Test routing function for an IPv6 IP packet with ECMP + + Send packets from first port and receive packets on any of second, third, forth ports + + Need to insert rules into: + 1) Ecmp group actions in the group table + 2) Vlan table to map packet to a vlan + 3) Mac Term table to send router's mac packets to the routing table + 4) the actual routing rule in the unicast routing table + + Then test from all ports to all ports + + """ + + + def runTest(self): + ports = sorted(config["port_map"].keys()) + + # FIXME test to make sure we have at least 6 ports + + delete_all_flows(self.controller) + delete_all_groups(self.controller) + do_barrier(self.controller) + + + ifaces = [ + # mac,vlan,port, dst_mac, dst_ip + L3Iface('00:11:11:11:11:11', 1, 1, '00:11:11:11:11:ff', 'fe80::1111'), + L3Iface('00:22:22:22:22:22', 2, 2, '00:22:22:22:22:ff', 'fe80::2222'), + L3Iface('00:33:33:33:33:33', 3, 3, '00:33:33:33:33:ff', 'fe80::3333'), + L3Iface('00:44:44:44:44:44', 4, 4, '00:44:44:44:44:ff', 'fe80::4444'), + #L3Iface('00:55:55:55:55:55', 5, 5, '00:55:55:55:55:ff', 'fe80::5555'), + #L3Iface('00:66:66:66:66:66', 6, 6, '00:66:66:66:66:ff', 'fe80::6666'), + ] + + for iface in ifaces: + self.create_interface(iface) + + ecmp_dst_ip = 'fe80::eeee' # stuff sent to this address should be split + # across the other interfaces + ecmp_dst_prefix = 'fe80::ee00' + ecmp_dst_mask = ':'.join(['ffff'] * 7) + ":ff00" # "ff:ff:ff:...:ff:00" + logging.info("ECMP prefix is %s/%s" % (ecmp_dst_prefix, ecmp_dst_mask)) + + ecmp_group_id = ofdpa_utils.makeGroupID("L3 ECMP", 1) + self.send_log_check("Creating L3 ECMP group", ofp.message.group_add( + group_type=ofp.OFPGT_SELECT, + group_id = ecmp_group_id, + buckets = + # make a list of the group ID's for ports 2-4 + map( (lambda iface : ofp.bucket( actions = [ofp.action.group(iface.l3_group_id) ] )), + ifaces[1:]) + )) + + self.send_log_check("Adding IPv6 route rule to ECMP output to ports 2-4" , ofp.message.flow_add( + table_id = ofdpa_utils.UNICAST_ROUTING_TABLE.table_id, + cookie=77, + match= ofp.match([ + ofp.oxm.eth_type(0x86dd), # ethertype = IPv6 + ofp.oxm.ipv6_dst_masked(parse_ipv6(ecmp_dst_prefix), parse_ipv6(ecmp_dst_mask)), + ]), + instructions=[ + ofp.instruction.goto_table(ofdpa_utils.ACL_TABLE.table_id), + ofp.instruction.write_actions([ + ofp.action.group(ecmp_group_id) + ]) + ], + buffer_id=ofp.OFP_NO_BUFFER, + priority=1000)) + + for iface in ifaces[1:]: + iface.count = 0 + + # test sending lots of packets from iface 0 to ifaces 1-3 + maxPackets = 20 + for src in range(maxPackets): + ip_src = 'fe80::01:%.4d' % src # give each packet a unique source + parsed_pkt = testutils12.simple_ipv6_packet( + ip_src = ip_src, + ip_dst = ecmp_dst_ip, + dl_dst = ifaces[0].mac) + pkt = str(parsed_pkt) + + logging.info("IPv6_Untagged_ECMP test, sending from iface %d to ECMP group", ifaces[0]) + self.dataplane.send(ifaces[0].port, pkt) + found = False + for iface in ifaces[1:]: + logging.debug("Checking for ECMP pkt on port %d", iface.port) + (rcv_port, rcv_pkt, pkt_time) = self.dataplane.poll(port_number=iface.port) + if rcv_pkt is not None: + parsed_rcv = oftest.parse.packet_to_flow_match(rcv_pkt) + if rcv_pkt.dl_dst == iface.dst_mac and \ + rcv_pkt.dl_src == iface.mac and \ + rcv_pkt.ip_dst == ecmp_dst_ip and \ + rcv_pkt.hlim == 63: # was TTL decremented? + found = True + iface.count += 1 + break + + test.assertTrue(found != None, "Did not receive pkt on Any ECMP interface") + # now make sure each interface got at least one packet; this is statistical so could fail if + # max packets is not high enough + for iface in ifaces[1:]: + test.assertTrue(iface.count > 0, "L3 interface %d did not receive any packets" % iface.port) + diff --git a/tests-ofdpa-2.0/ofdpa_utils.py b/tests-ofdpa-2.0/ofdpa_utils.py new file mode 100644 index 000000000..ed5259027 --- /dev/null +++ b/tests-ofdpa-2.0/ofdpa_utils.py @@ -0,0 +1,155 @@ +""" +A set of inter-test utility functions for dealing with OF-DPA + + +""" + +import logging +import ofp + +from oftest import config +from oftest.testutils import * + + + +class table(object): + """ Metadata on each OFDPA table """ + def __init__(self, table_id, table_name): + self.table_id = table_id + self.table_name = table_name + # TODO consider adding type checking verification here + +INGRESS_TABLE = table(0, "Ingress") +VLAN_TABLE = table(10, "VLAN") +MACTERM_TABLE = table(20, "MacTerm") +UNICAST_ROUTING_TABLE = table(30, "Unicast Routing") +MULTICAST_ROUTING_TABLE = table(40, "Multicast Routing") +BRIDGING_TABLE = table(50, "Bridging Table") +ACL_TABLE = table(60, "ACL Policy Table") +#.... FIXME add all tables + +DEFAULT_VLAN = 1 + +def enableVlanOnPort(controller, vlan, port=ofp.OFPP_ALL, priority=0): + if port == ofp.OFPP_ALL: + ports = sorted(config["port_map"].keys()) + else: + ports = [port] + for port in ports: + tagged_match = ofp.match([ + ofp.oxm.in_port(port), + ofp.oxm.vlan_vid(vlan | ofp.OFPVID_PRESENT) + ]) + + request = ofp.message.flow_add( + table_id = VLAN_TABLE.table_id, + cookie = 0xdead, + match = tagged_match, + instructions = [ + ofp.instruction.goto_table(MACTERM_TABLE.table_id), +# ofp.instruction.apply_actions( +# actions=[ +# ofp.action.push_vlan(ethertype=0x8100), # DO NOT PUT THIS FOR OF-DPA 2.0 EA1 - seems to not matter for EA2 +# ofp.action.set_field(ofp.oxm.vlan_vid( ofp.OFPVID_PRESENT | vlan)) +# ]), + ], + buffer_id = ofp.OFP_NO_BUFFER, + priority = priority) + + logging.info("Inserting vlan rule allowing tagged vlan %d on port %d" % (vlan, port)) + controller.message_send(request) + do_barrier(controller) + verify_no_errors(controller) + + +def installDefaultVlan(controller, vlan=DEFAULT_VLAN, port=ofp.OFPP_ALL, priority=0): + """ Insert a rule that maps all untagged traffic to vlan $vlan + + In OFDPA, table 10 (the vlan table) requires that all traffic be + mapped to an internal vlan else the packets be dropped. This function + sets up a default vlan mapping all untagged traffic to an internal VLAN. + + With OF-DPA, before you can insert a 'untagged to X' rule on a + port, you must first insert a 'X --> X' rule for the same port. + + Further, the 'X --> X' rule must set ofp.OFPVID_PRESENT even + though 'X' is non-zero. + + The 'controller' variable is self.controller from a test + """ + # OFDPA seems to be dumb and wants each port set individually + # Can't set all ports by using OFPP_ALL + if port == ofp.OFPP_ALL: + ports = sorted(config["port_map"].keys()) + else: + ports = [port] + + for port in ports: + # enable this vlan on this port before we can map untagged packets to the vlan + enableVlanOnPort(controller, vlan, port) + + untagged_match = ofp.match([ + ofp.oxm.in_port(port), + # OFDPA 2.0 says untagged is vlan_id=0, mask=ofp.OFPVID_PRESENT + ofp.oxm.vlan_vid_masked(0,ofp.OFPVID_PRESENT) # WTF OFDPA 2.0EA2 -- really!? + ]) + + request = ofp.message.flow_add( + table_id = VLAN_TABLE.table_id, + cookie = 0xbeef, + match = untagged_match, + instructions = [ + ofp.instruction.apply_actions( + actions=[ + #ofp.action.push_vlan(ethertype=0x8100), + ofp.action.set_field(ofp.oxm.vlan_vid(ofp.OFPVID_PRESENT | vlan)) + ]), + ofp.instruction.goto_table(MACTERM_TABLE.table_id) + ], + buffer_id = ofp.OFP_NO_BUFFER, + priority = priority) + + logging.info("Inserting default vlan sending all untagged traffic to vlan %d on port %d" % (vlan, port)) + controller.message_send(request) + do_barrier(controller) + verify_no_errors(controller) + + +_group_types = { + "L2 Interface": 0, + "L2 Rewrite" : 1, + "L3 Unicast" : 2, + "L2 Multicast" : 3, + "L2 Flood" : 4, + "L3 Interface" : 5, + "L3 Multicast": 6, + "L3 ECMP": 7, + "L2 Data Center Overlay": 8, + "MPLS Label" : 9, + "MPLS Forwarding" :10, + "L2 Unfiltered Interface": 11, + "L2 Loopback": 12, +} + + +def makeGroupID(groupType, local_id): + """ Group IDs in OF-DPA have rich meaning + + @param groupType is a key in _group_types + @param local_id is an integer 0<= local_id < 2**27, + but it may have more semantic meaning depending on the + groupType + + + Read Section 4.3 of the OF-DPA manual on groups for + details + """ + if groupType not in _group_types: + raise KeyError("%s not a valid OF-DPA group type" % groupType) + if local_id < 0 or local_id >=134217728: + raise ValueError("local_id %d must be 0<= local_id < 2**27" % local_id) + return (_group_types[groupType] << 28) + local_id + + +def delete_all_recursive_groups(controller): + pass