Some major restructuring

Updated README with some warnings.
Added data-plane-only class to allow controlling the dataplane
ports without needing a controller connection.
Subclassed this to allow sending only a packet without doing
any flow mods; both tagged or untagged.
Added the ability to pass a parameter to a test through th
config structure.  Use --param=N.
Used the above to set the VLAN id in a tagged pkt in the new test.
Break up description/name in --list when name is long

Restructured pktact.py into different routines, moving a lot of
the base functionality into testutils.py.  This allows packet
modification tests to be done independently.

In the process, added support for using tagged and untagged
packets in the tests.  Several tests remain to be implemented.
diff --git a/tests/testutils.py b/tests/testutils.py
index ea82ecc..044e5c8 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -18,6 +18,11 @@
 import oftest.parse as parse
 import logging
 
+# Some useful defines
+IP_ETHERTYPE = 0x800
+TCP_PROTOCOL = 0x6
+UDP_PROTOCOL = 0x11
+
 def delete_all_flows(ctrl, logger):
     """
     Delete all flows on the switch
@@ -39,6 +44,7 @@
                       dl_vlan_enable=False,
                       dl_vlan=0,
                       dl_vlan_pcp=0,
+                      dl_vlan_cfi=0,
                       ip_src='192.168.0.1',
                       ip_dst='192.168.0.2',
                       ip_tos=0,
@@ -65,9 +71,10 @@
     shouldn't assume anything about this packet other than that
     it is a valid ethernet/IP/TCP frame.
     """
+    # Note Dot1Q.id is really CFI
     if (dl_vlan_enable):
         pkt = scapy.Ether(dst=dl_dst, src=dl_src)/ \
-            scapy.Dot1Q(prio=dl_vlan_pcp, id=0, vlan=dl_vlan)/ \
+            scapy.Dot1Q(prio=dl_vlan_pcp, id=dl_vlan_cfi, vlan=dl_vlan)/ \
             scapy.IP(src=ip_src, dst=ip_dst, tos=ip_tos)/ \
             scapy.TCP(sport=tcp_sport, dport=tcp_dport)
     else:
@@ -206,3 +213,263 @@
             port_number=ofport, timeout=1)
         assert_if.assertTrue(rcv_pkt is None, 
                              "Unexpected pkt on port " + str(ofport))
+
+
+def receive_pkt_verify(parent, egr_port, exp_pkt):
+    """
+    Receive a packet and verify it matches an expected value
+
+    parent must implement dataplane, assertTrue and assertEqual
+    """
+    (rcv_port, rcv_pkt, pkt_time) = parent.dataplane.poll(port_number=egr_port,
+                                                          timeout=1)
+    if rcv_pkt is None:
+        parent.logger.error("ERROR: No packet received from " + str(egr_port))
+
+    parent.assertTrue(rcv_pkt is not None,
+                      "Did not receive packet port " + str(egr_port))
+    parent.logger.debug("Packet len " + str(len(rcv_pkt)) + " in on " + 
+                    str(rcv_port))
+
+    if str(exp_pkt) != str(rcv_pkt):
+        parent.logger.error("ERROR: Packet match failed.")
+        parent.logger.debug("Expected len " + str(len(exp_pkt)) + ": "
+                        + str(exp_pkt).encode('hex'))
+        parent.logger.debug("Received len " + str(len(rcv_pkt)) + ": "
+                        + str(rcv_pkt).encode('hex'))
+    parent.assertEqual(str(exp_pkt), str(rcv_pkt),
+                       "Packet match error on port " + str(egr_port))
+    
+def match_verify(parent, req_match, res_match):
+    """
+    Verify flow matches agree; if they disagree, report where
+
+    parent must implement assertEqual
+    Use str() to ensure content is compared and not pointers
+    """
+
+    parent.assertEqual(req_match.wildcards, res_match.wildcards,
+                       'Match failed: wildcards: ' + hex(req_match.wildcards) +
+                       " != " + hex(res_match.wildcards))
+    parent.assertEqual(req_match.in_port, res_match.in_port,
+                       'Match failed: in_port: ' + str(req_match.in_port) +
+                       " != " + str(res_match.in_port))
+    parent.assertEqual(str(req_match.dl_src), str(res_match.dl_src),
+                       'Match failed: dl_src: ' + str(req_match.dl_src) +
+                       " != " + str(res_match.dl_src))
+    parent.assertEqual(str(req_match.dl_dst), str(res_match.dl_dst),
+                       'Match failed: dl_dst: ' + str(req_match.dl_dst) +
+                       " != " + str(res_match.dl_dst))
+    parent.assertEqual(req_match.dl_vlan, res_match.dl_vlan,
+                       'Match failed: dl_vlan: ' + str(req_match.dl_vlan) +
+                       " != " + str(res_match.dl_vlan))
+    parent.assertEqual(req_match.dl_vlan_pcp, res_match.dl_vlan_pcp,
+                       'Match failed: dl_vlan_pcp: ' + 
+                       str(req_match.dl_vlan_pcp) + " != " + 
+                       str(res_match.dl_vlan_pcp))
+    parent.assertEqual(req_match.dl_type, res_match.dl_type,
+                       'Match failed: dl_type: ' + str(req_match.dl_type) +
+                       " != " + str(res_match.dl_type))
+
+    if (not(req_match.wildcards & ofp.OFPFW_DL_TYPE)
+        and (req_match.dl_type == IP_ETHERTYPE)):
+        parent.assertEqual(req_match.nw_tos, res_match.nw_tos,
+                           'Match failed: nw_tos: ' + str(req_match.nw_tos) +
+                           " != " + str(res_match.nw_tos))
+        parent.assertEqual(req_match.nw_proto, res_match.nw_proto,
+                           'Match failed: nw_proto: ' + str(req_match.nw_proto) +
+                           " != " + str(res_match.nw_proto))
+        parent.assertEqual(req_match.nw_src, res_match.nw_src,
+                           'Match failed: nw_src: ' + str(req_match.nw_src) +
+                           " != " + str(res_match.nw_src))
+        parent.assertEqual(req_match.nw_dst, res_match.nw_dst,
+                           'Match failed: nw_dst: ' + str(req_match.nw_dst) +
+                           " != " + str(res_match.nw_dst))
+
+        if (not(req_match.wildcards & ofp.OFPFW_NW_PROTO)
+            and ((req_match.nw_proto == TCP_PROTOCOL)
+                 or (req_match.nw_proto == UDP_PROTOCOL))):
+            parent.assertEqual(req_match.tp_src, res_match.tp_src,
+                               'Match failed: tp_src: ' + 
+                               str(req_match.tp_src) +
+                               " != " + str(res_match.tp_src))
+            parent.assertEqual(req_match.tp_dst, res_match.tp_dst,
+                               'Match failed: tp_dst: ' + 
+                               str(req_match.tp_dst) +
+                               " != " + str(res_match.tp_dst))
+
+def flow_removed_verify(parent, request=None, pkt_count=-1, byte_count=-1):
+    """
+    Receive a flow removed msg and verify it matches expected
+
+    @params parent Must implement controller, assertEqual
+    @param pkt_count If >= 0, verify packet count
+    @param byte_count If >= 0, verify byte count
+    """
+    (response, raw) = parent.controller.poll(ofp.OFPT_FLOW_REMOVED, 2)
+    parent.assertTrue(response is not None, 'No flow removed message received')
+
+    if request is None:
+        return
+
+    parent.assertEqual(request.cookie, response.cookie,
+                       "Flow removed cookie error: " +
+                       hex(request.cookie) + " != " + hex(response.cookie))
+
+    req_match = request.match
+    res_match = response.match
+    verifyMatchField(req_match, res_match)
+
+    if (req_match.wildcards != 0):
+        parent.assertEqual(request.priority, response.priority,
+                           'Flow remove prio mismatch: ' + 
+                           str(request,priority) + " != " + 
+                           str(response.priority))
+        parent.assertEqual(response.reason, ofp.OFPRR_HARD_TIMEOUT,
+                           'Flow remove reason is not HARD TIMEOUT:' +
+                           str(response.reason))
+        if pkt_count >= 0:
+            parent.assertEqual(response.packet_count, pkt_count,
+                               'Flow removed failed, packet count: ' + 
+                               str(response.packet_count) + " != " +
+                               str(pkt_count))
+        if byte_count >= 0:
+            parent.assertEqual(response.byte_count, byte_count,
+                               'Flow removed failed, byte count: ' + 
+                               str(response.byte_count) + " != " + 
+                               str(byte_count))
+
+def flow_msg_create(parent, pkt, ing_port=None, action_list=None, wildcards=0,
+               egr_port=None, egr_queue=None, check_expire=False):
+    """
+    Create a flow message
+
+    Match on packet with given wildcards.  
+    See flow_match_test for other parameter descriptoins
+    @param egr_queue if not None, make the output an enqueue action
+    """
+    match = parse.packet_to_flow_match(pkt)
+    parent.assertTrue(match is not None, "Flow match from pkt failed")
+    match.wildcards = wildcards
+    match.in_port = ing_port
+
+    request = message.flow_mod()
+    request.match = match
+    request.buffer_id = 0xffffffff
+    if check_expire:
+        request.flags |= ofp.OFPFF_SEND_FLOW_REM
+        request.hard_timeout = 1
+
+    if action_list is not None:
+        for act in action_list:
+            parent.logger.debug("Adding action " + act.show())
+            rv = request.actions.add(act)
+            parent.assertTrue(rv, "Could not add action" + act.show())
+
+    # Set up output/enqueue action if directed
+    if egr_queue is not None:
+        parent.assertTrue(egr_port is not None, "Egress port not set")
+        act = action.action_enqueue()
+        act.port = egr_port
+        act.queue_id = egr_queue
+        rv = request.actions.add(act)
+        parent.assertTrue(rv, "Could not add enqueue action " + 
+                          str(egr_port) + " Q: " + str(egr_queue))
+    elif egr_port is not None:
+        act = action.action_output()
+        act.port = egr_port
+        rv = request.actions.add(act)
+        parent.assertTrue(rv, "Could not add output action " + str(egr_port))
+
+    parent.logger.debug(request.show())
+
+    return request
+
+def flow_msg_install(parent, request, clear_table=True):
+    """
+    Install a flow mod message in the switch
+
+    @param parent Must implement controller, assertEqual, assertTrue
+    @param request The request, all set to go
+    @param clear_table If true, clear the flow table before installing
+    """
+    if clear_table:
+        parent.logger.debug("Clear flow table")
+        rc = delete_all_flows(parent.controller, parent.logger)
+        parent.assertEqual(rc, 0, "Failed to delete all flows")
+        do_barrier(parent.controller)
+
+    parent.logger.debug("Insert flow")
+    rv = parent.controller.message_send(request)
+    parent.assertTrue(rv != -1, "Error installing flow mod")
+    do_barrier(parent.controller)
+
+def flow_match_test_port_pair(parent, ing_port, egr_port, wildcards=0, 
+                              dl_vlan=-1, pkt=None, exp_pkt=None,
+                              action_list=None, check_expire=False):
+    """
+    Flow match test on single TCP packet
+
+    Run test with packet through switch from ing_port to egr_port
+    See flow_match_test for parameter descriptions
+    """
+
+    parent.logger.info("Pkt match test: " + str(ing_port) + " to " + str(egr_port))
+    parent.logger.debug("  WC: " + hex(wildcards) + " vlan: " + str(dl_vlan) +
+                    " exp: " + str(check_expire))
+    if pkt is None:
+        pkt = simple_tcp_packet(dl_vlan_enable=(dl_vlan >= 0), dl_vlan=dl_vlan)
+
+    request = flow_msg_create(parent, pkt, ing_port=ing_port, 
+                              wildcards=wildcards, egr_port=egr_port,
+                              action_list=action_list)
+
+    flow_msg_install(parent, request)
+
+    parent.logger.debug("Send packet: " + str(ing_port) + " to " + str(egr_port))
+    parent.dataplane.send(ing_port, str(pkt))
+
+    if exp_pkt is None:
+        exp_pkt = pkt
+    receive_pkt_verify(parent, egr_port, exp_pkt)
+
+    if check_expire:
+        #@todo Not all HW supports both pkt and byte counters
+        flow_removed_verify(parent, request, pkt_count=1, byte_count=len(pkt))
+
+def flow_match_test(parent, port_map, wildcards=0, dl_vlan=-1, pkt=None, 
+                    exp_pkt=None, action_list=None, check_expire=False, 
+                    max_test=0):
+    """
+    Run flow_match_test_port_pair on all port pairs
+
+    @param max_test If > 0 no more than this number of tests are executed.
+    @param parent Must implement controller, dataplane, assertTrue, assertEqual
+    and logger
+    @param pkt If not None, use this packet for ingress
+    @param wildcards For flow match entry
+    @param dl_vlan If not -1, and pkt is not None, create a pkt w/ VLAN tag
+    @param exp_pkt If not None, use this as the expected output pkt; els use pkt
+    @param action_list Additional actions to add to flow mod
+    @param check_expire Check for flow expiration message
+    """
+    of_ports = port_map.keys()
+    of_ports.sort()
+    parent.assertTrue(len(of_ports) > 1, "Not enough ports for test")
+    test_count = 0
+
+    for ing_idx in range(len(of_ports)):
+        ingress_port = of_ports[ing_idx]
+        for egr_idx in range(len(of_ports)):
+            if egr_idx == ing_idx:
+                continue
+            egress_port = of_ports[egr_idx]
+            flow_match_test_port_pair(parent, ingress_port, egress_port, 
+                                      dl_vlan=dl_vlan, pkt=pkt, 
+                                      exp_pkt=exp_pkt, action_list=action_list,
+                                      check_expire=check_expire)
+            test_count += 1
+            if (max_test > 0) and (test_count > max_test):
+                parent.logger.info("Ran " + str(test_count) + " tests; exiting")
+                return
+