Merge pull request #60 from rlane/actions

initial OF 1.3 actions tests
diff --git a/src/python/loxi/generic_util.py b/src/python/loxi/generic_util.py
index ce31049..f965e64 100644
--- a/src/python/loxi/generic_util.py
+++ b/src/python/loxi/generic_util.py
@@ -40,6 +40,13 @@
         return deserializer(reader.slice(length), typ)
     return unpack_list(reader, wrapper)
 
+def pad_to(alignment, length):
+    """
+    Return a string of zero bytes that will pad a string of length 'length' to
+    a multiple of 'alignment'.
+    """
+    return "\x00" * ((length + alignment - 1)/alignment*alignment - length)
+
 class OFReader(object):
     """
     Cursor over a read-only buffer
@@ -79,6 +86,12 @@
             raise loxi.ProtocolError("Buffer too short")
         self.offset += length
 
+    def skip_align(self):
+        new_offset = (self.offset + 7) / 8 * 8
+        if new_offset > len(self.buf):
+            raise loxi.ProtocolError("Buffer too short")
+        self.offset = new_offset
+
     def is_empty(self):
         return self.offset == len(self.buf)
 
diff --git a/src/python/loxi/of10/const.py b/src/python/loxi/of10/const.py
index 1d29975..dc768c6 100644
--- a/src/python/loxi/of10/const.py
+++ b/src/python/loxi/of10/const.py
@@ -111,6 +111,15 @@
     65535: 'OF_BSN_VPORT_Q_IN_Q_UNTAGGED',
 }
 
+# Identifiers from group ofp_bsn_vport_status
+OF_BSN_VPORT_STATUS_OK = 0
+OF_BSN_VPORT_STATUS_FAILED = 1
+
+ofp_bsn_vport_status_map = {
+    0: 'OF_BSN_VPORT_STATUS_OK',
+    1: 'OF_BSN_VPORT_STATUS_FAILED',
+}
+
 # Identifiers from group ofp_capabilities
 OFPC_FLOW_STATS = 1
 OFPC_TABLE_STATS = 2
diff --git a/src/python/loxi/of11/const.py b/src/python/loxi/of11/const.py
index bd0fd11..4360d37 100644
--- a/src/python/loxi/of11/const.py
+++ b/src/python/loxi/of11/const.py
@@ -186,6 +186,15 @@
     65535: 'OF_BSN_VPORT_Q_IN_Q_UNTAGGED',
 }
 
+# Identifiers from group ofp_bsn_vport_status
+OF_BSN_VPORT_STATUS_OK = 0
+OF_BSN_VPORT_STATUS_FAILED = 1
+
+ofp_bsn_vport_status_map = {
+    0: 'OF_BSN_VPORT_STATUS_OK',
+    1: 'OF_BSN_VPORT_STATUS_FAILED',
+}
+
 # Identifiers from group ofp_capabilities
 OFPC_FLOW_STATS = 1
 OFPC_TABLE_STATS = 2
diff --git a/src/python/loxi/of12/action.py b/src/python/loxi/of12/action.py
index f1f9272..9ae3d8f 100644
--- a/src/python/loxi/of12/action.py
+++ b/src/python/loxi/of12/action.py
@@ -11,6 +11,7 @@
 import util
 import loxi.generic_util
 import loxi
+import oxm # for unpack
 
 def unpack_list(reader):
     def deserializer(reader, typ):
@@ -756,15 +757,17 @@
         if field != None:
             self.field = field
         else:
-            self.field = ''
+            self.field = None
         return
 
     def pack(self):
         packed = []
         packed.append(struct.pack("!H", self.type))
         packed.append(struct.pack("!H", 0)) # placeholder for len at index 1
-        packed.append(self.field)
+        packed.append(self.field.pack())
         length = sum([len(x) for x in packed])
+        packed.append(loxi.generic_util.pad_to(8, length))
+        length += len(packed[-1])
         packed[1] = struct.pack("!H", length)
         return ''.join(packed)
 
@@ -778,7 +781,8 @@
         _type = reader.read("!H")[0]
         assert(_type == 25)
         _len = reader.read("!H")[0]
-        obj.field = str(reader.read_all())
+        obj.field = oxm.unpack(reader)
+        reader.skip_align()
         return obj
 
     def __eq__(self, other):
diff --git a/src/python/loxi/of12/common.py b/src/python/loxi/of12/common.py
index f2d2493..698fe7a 100644
--- a/src/python/loxi/of12/common.py
+++ b/src/python/loxi/of12/common.py
@@ -706,7 +706,7 @@
         packed.append(util.pack_list(self.oxm_list))
         length = sum([len(x) for x in packed])
         packed[1] = struct.pack("!H", length)
-        packed.append('\x00' * ((length + 7)/8*8 - length))
+        packed.append(loxi.generic_util.pad_to(8, length))
         return ''.join(packed)
 
     @staticmethod
@@ -720,7 +720,7 @@
         assert(_type == 1)
         _length = reader.read("!H")[0]
         obj.oxm_list = oxm.unpack_list(reader.slice(_length-4))
-        reader.skip((_length + 7)/8*8 - _length)
+        reader.skip_align()
         return obj
 
     def __eq__(self, other):
diff --git a/src/python/loxi/of12/const.py b/src/python/loxi/of12/const.py
index 20f99bb..b654b1a 100644
--- a/src/python/loxi/of12/const.py
+++ b/src/python/loxi/of12/const.py
@@ -187,6 +187,15 @@
     65535: 'OF_BSN_VPORT_Q_IN_Q_UNTAGGED',
 }
 
+# Identifiers from group ofp_bsn_vport_status
+OF_BSN_VPORT_STATUS_OK = 0
+OF_BSN_VPORT_STATUS_FAILED = 1
+
+ofp_bsn_vport_status_map = {
+    0: 'OF_BSN_VPORT_STATUS_OK',
+    1: 'OF_BSN_VPORT_STATUS_FAILED',
+}
+
 # Identifiers from group ofp_capabilities
 OFPC_FLOW_STATS = 1
 OFPC_TABLE_STATS = 2
diff --git a/src/python/loxi/of12/oxm.py b/src/python/loxi/of12/oxm.py
index f17151e..0fd7948 100644
--- a/src/python/loxi/of12/oxm.py
+++ b/src/python/loxi/of12/oxm.py
@@ -12,15 +12,16 @@
 import loxi.generic_util
 import loxi
 
+def unpack(reader):
+    type_len, = reader.peek('!L')
+    if type_len in parsers:
+        return parsers[type_len](reader)
+    else:
+        raise loxi.ProtocolError("unknown OXM cls=%#x type=%#x masked=%d len=%d (%#x)" % \
+            ((type_len >> 16) & 0xffff, (type_len >> 9) & 0x7f, (type_len >> 8) & 1, type_len & 0xff, type_len))
+
 def unpack_list(reader):
-    def deserializer(reader):
-        type_len, = reader.peek('!L')
-        if type_len in parsers:
-            return parsers[type_len](reader)
-        else:
-            raise loxi.ProtocolError("unknown OXM cls=%#x type=%#x masked=%d len=%d (%#x)" % \
-                ((type_len >> 16) & 0xffff, (type_len >> 9) & 0x7f, (type_len >> 8) & 1, type_len & 0xff, type_len))
-    return loxi.generic_util.unpack_list(reader, deserializer)
+    return loxi.generic_util.unpack_list(reader, unpack)
 
 class OXM(object):
     type_len = None # override in subclass
@@ -1907,7 +1908,7 @@
             with q.indent(2):
                 q.breakable()
                 q.text("value = ");
-                q.text("%#x" % self.value)
+                q.text(util.pretty_ipv4(self.value))
             q.breakable()
         q.text('}')
 
@@ -1963,10 +1964,10 @@
             with q.indent(2):
                 q.breakable()
                 q.text("value = ");
-                q.text("%#x" % self.value)
+                q.text(util.pretty_ipv4(self.value))
                 q.text(","); q.breakable()
                 q.text("value_mask = ");
-                q.text("%#x" % self.value_mask)
+                q.text(util.pretty_ipv4(self.value_mask))
             q.breakable()
         q.text('}')
 
@@ -2015,7 +2016,7 @@
             with q.indent(2):
                 q.breakable()
                 q.text("value = ");
-                q.text("%#x" % self.value)
+                q.text(util.pretty_ipv4(self.value))
             q.breakable()
         q.text('}')
 
@@ -2071,10 +2072,10 @@
             with q.indent(2):
                 q.breakable()
                 q.text("value = ");
-                q.text("%#x" % self.value)
+                q.text(util.pretty_ipv4(self.value))
                 q.text(","); q.breakable()
                 q.text("value_mask = ");
-                q.text("%#x" % self.value_mask)
+                q.text(util.pretty_ipv4(self.value_mask))
             q.breakable()
         q.text('}')
 
diff --git a/src/python/loxi/of13/action.py b/src/python/loxi/of13/action.py
index 645916a..a1e7877 100644
--- a/src/python/loxi/of13/action.py
+++ b/src/python/loxi/of13/action.py
@@ -11,6 +11,7 @@
 import util
 import loxi.generic_util
 import loxi
+import oxm # for unpack
 
 def unpack_list(reader):
     def deserializer(reader, typ):
@@ -859,15 +860,17 @@
         if field != None:
             self.field = field
         else:
-            self.field = ''
+            self.field = None
         return
 
     def pack(self):
         packed = []
         packed.append(struct.pack("!H", self.type))
         packed.append(struct.pack("!H", 0)) # placeholder for len at index 1
-        packed.append(self.field)
+        packed.append(self.field.pack())
         length = sum([len(x) for x in packed])
+        packed.append(loxi.generic_util.pad_to(8, length))
+        length += len(packed[-1])
         packed[1] = struct.pack("!H", length)
         return ''.join(packed)
 
@@ -881,7 +884,8 @@
         _type = reader.read("!H")[0]
         assert(_type == 25)
         _len = reader.read("!H")[0]
-        obj.field = str(reader.read_all())
+        obj.field = oxm.unpack(reader)
+        reader.skip_align()
         return obj
 
     def __eq__(self, other):
diff --git a/src/python/loxi/of13/common.py b/src/python/loxi/of13/common.py
index e3f9921..952aeba 100644
--- a/src/python/loxi/of13/common.py
+++ b/src/python/loxi/of13/common.py
@@ -781,7 +781,7 @@
         packed.append(util.pack_list(self.oxm_list))
         length = sum([len(x) for x in packed])
         packed[1] = struct.pack("!H", length)
-        packed.append('\x00' * ((length + 7)/8*8 - length))
+        packed.append(loxi.generic_util.pad_to(8, length))
         return ''.join(packed)
 
     @staticmethod
@@ -795,7 +795,7 @@
         assert(_type == 1)
         _length = reader.read("!H")[0]
         obj.oxm_list = oxm.unpack_list(reader.slice(_length-4))
-        reader.skip((_length + 7)/8*8 - _length)
+        reader.skip_align()
         return obj
 
     def __eq__(self, other):
diff --git a/src/python/loxi/of13/const.py b/src/python/loxi/of13/const.py
index e2545f1..648617f 100644
--- a/src/python/loxi/of13/const.py
+++ b/src/python/loxi/of13/const.py
@@ -193,6 +193,15 @@
     65535: 'OF_BSN_VPORT_Q_IN_Q_UNTAGGED',
 }
 
+# Identifiers from group ofp_bsn_vport_status
+OF_BSN_VPORT_STATUS_OK = 0
+OF_BSN_VPORT_STATUS_FAILED = 1
+
+ofp_bsn_vport_status_map = {
+    0: 'OF_BSN_VPORT_STATUS_OK',
+    1: 'OF_BSN_VPORT_STATUS_FAILED',
+}
+
 # Identifiers from group ofp_capabilities
 OFPC_FLOW_STATS = 1
 OFPC_TABLE_STATS = 2
diff --git a/src/python/loxi/of13/oxm.py b/src/python/loxi/of13/oxm.py
index f17151e..0fd7948 100644
--- a/src/python/loxi/of13/oxm.py
+++ b/src/python/loxi/of13/oxm.py
@@ -12,15 +12,16 @@
 import loxi.generic_util
 import loxi
 
+def unpack(reader):
+    type_len, = reader.peek('!L')
+    if type_len in parsers:
+        return parsers[type_len](reader)
+    else:
+        raise loxi.ProtocolError("unknown OXM cls=%#x type=%#x masked=%d len=%d (%#x)" % \
+            ((type_len >> 16) & 0xffff, (type_len >> 9) & 0x7f, (type_len >> 8) & 1, type_len & 0xff, type_len))
+
 def unpack_list(reader):
-    def deserializer(reader):
-        type_len, = reader.peek('!L')
-        if type_len in parsers:
-            return parsers[type_len](reader)
-        else:
-            raise loxi.ProtocolError("unknown OXM cls=%#x type=%#x masked=%d len=%d (%#x)" % \
-                ((type_len >> 16) & 0xffff, (type_len >> 9) & 0x7f, (type_len >> 8) & 1, type_len & 0xff, type_len))
-    return loxi.generic_util.unpack_list(reader, deserializer)
+    return loxi.generic_util.unpack_list(reader, unpack)
 
 class OXM(object):
     type_len = None # override in subclass
@@ -1907,7 +1908,7 @@
             with q.indent(2):
                 q.breakable()
                 q.text("value = ");
-                q.text("%#x" % self.value)
+                q.text(util.pretty_ipv4(self.value))
             q.breakable()
         q.text('}')
 
@@ -1963,10 +1964,10 @@
             with q.indent(2):
                 q.breakable()
                 q.text("value = ");
-                q.text("%#x" % self.value)
+                q.text(util.pretty_ipv4(self.value))
                 q.text(","); q.breakable()
                 q.text("value_mask = ");
-                q.text("%#x" % self.value_mask)
+                q.text(util.pretty_ipv4(self.value_mask))
             q.breakable()
         q.text('}')
 
@@ -2015,7 +2016,7 @@
             with q.indent(2):
                 q.breakable()
                 q.text("value = ");
-                q.text("%#x" % self.value)
+                q.text(util.pretty_ipv4(self.value))
             q.breakable()
         q.text('}')
 
@@ -2071,10 +2072,10 @@
             with q.indent(2):
                 q.breakable()
                 q.text("value = ");
-                q.text("%#x" % self.value)
+                q.text(util.pretty_ipv4(self.value))
                 q.text(","); q.breakable()
                 q.text("value_mask = ");
-                q.text("%#x" % self.value_mask)
+                q.text(util.pretty_ipv4(self.value_mask))
             q.breakable()
         q.text('}')
 
diff --git a/src/python/oftest/parse.py b/src/python/oftest/parse.py
index 8c98c91..7963c0c 100644
--- a/src/python/oftest/parse.py
+++ b/src/python/oftest/parse.py
@@ -196,7 +196,7 @@
         if type(layer.payload) == scapy.Dot1Q:
             layer = layer.payload
             match.oxm_list.append(ofp.oxm.eth_type(layer.type))
-            match.oxm_list.append(ofp.oxm.vlan_vid(layer.vlan))
+            match.oxm_list.append(ofp.oxm.vlan_vid(ofp.OFPVID_PRESENT|layer.vlan))
             match.oxm_list.append(ofp.oxm.vlan_pcp(layer.prio))
         else:
             match.oxm_list.append(ofp.oxm.eth_type(layer.type))
diff --git a/src/python/oftest/testutils.py b/src/python/oftest/testutils.py
index bf2b24a..e87b7a0 100644
--- a/src/python/oftest/testutils.py
+++ b/src/python/oftest/testutils.py
@@ -1598,4 +1598,17 @@
 
     test.assertTrue(msg == None, "Did not expect a packet-in message on port %d" % in_port)
 
+def openflow_ports(num=None):
+    """
+    Return a list of 'num' OpenFlow port numbers
+
+    If 'num' is None, return all available ports. Otherwise, limit the length
+    of the result to 'num' and raise an exception if not enough ports are
+    available.
+    """
+    ports = sorted(oftest.config["port_map"].keys())
+    if num != None and len(ports) < num:
+        raise Exception("test requires %d ports but only %d are available" % (num, len(ports)))
+    return ports[:num]
+
 __all__ = list(set(locals()) - _import_blacklist)
diff --git a/tests-1.3/actions.py b/tests-1.3/actions.py
new file mode 100644
index 0000000..39369d0
--- /dev/null
+++ b/tests-1.3/actions.py
@@ -0,0 +1,161 @@
+# 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.
+"""
+Action test cases
+
+These tests check the behavior of each type of action. The matches used are
+exact-match, to satisfy the OXM prerequisites of the set-field actions.
+These tests use a single apply-actions instruction.
+"""
+
+import logging
+
+from oftest import config
+import oftest.base_tests as base_tests
+import ofp
+from loxi.pp import pp
+
+from oftest.testutils import *
+from oftest.parse import parse_ipv6
+
+class Output(base_tests.SimpleDataPlane):
+    """
+    Output to a single port
+    """
+    def runTest(self):
+        in_port, out_port = openflow_ports(2)
+
+        actions = [ofp.action.output(out_port)]
+
+        pkt = simple_tcp_packet()
+
+        logging.info("Running actions test for %s", pp(actions))
+
+        delete_all_flows(self.controller)
+
+        logging.info("Inserting flow")
+        request = ofp.message.flow_add(
+                table_id=0,
+                match=packet_to_flow_match(self, pkt),
+                instructions=[
+                    ofp.instruction.apply_actions(actions)],
+                buffer_id=ofp.OFP_NO_BUFFER,
+                priority=1000)
+        self.controller.message_send(request)
+
+        do_barrier(self.controller)
+
+        pktstr = str(pkt)
+
+        logging.info("Sending packet, expecting output to port %d", out_port)
+        self.dataplane.send(in_port, pktstr)
+        receive_pkt_check(self.dataplane, pktstr, [out_port],
+                          set(openflow_ports()) - set([out_port]), self)
+
+class OutputMultiple(base_tests.SimpleDataPlane):
+    """
+    Output to three ports
+    """
+    def runTest(self):
+        ports = openflow_ports(4)
+        in_port = ports[0]
+        out_ports = ports[1:4]
+
+        actions = [ofp.action.output(x) for x in out_ports]
+
+        pkt = simple_tcp_packet()
+
+        logging.info("Running actions test for %s", pp(actions))
+
+        delete_all_flows(self.controller)
+
+        logging.info("Inserting flow")
+        request = ofp.message.flow_add(
+                table_id=0,
+                match=packet_to_flow_match(self, pkt),
+                instructions=[
+                    ofp.instruction.apply_actions(actions)],
+                buffer_id=ofp.OFP_NO_BUFFER,
+                priority=1000)
+        self.controller.message_send(request)
+
+        do_barrier(self.controller)
+
+        pktstr = str(pkt)
+
+        logging.info("Sending packet, expecting output to ports %r", out_ports)
+        self.dataplane.send(in_port, pktstr)
+        receive_pkt_check(self.dataplane, pktstr, out_ports,
+                          set(openflow_ports()) - set(out_ports), self)
+
+class BaseModifyPacketTest(base_tests.SimpleDataPlane):
+    """
+    Base class for action tests that modify a packet
+    """
+
+    def verify_modify(self, actions, pkt, exp_pkt):
+        in_port, out_port = openflow_ports(2)
+
+        actions = actions + [ofp.action.output(out_port)]
+
+        logging.info("Running actions test for %s", pp(actions))
+
+        delete_all_flows(self.controller)
+
+        logging.info("Inserting flow")
+        request = ofp.message.flow_add(
+                table_id=0,
+                match=packet_to_flow_match(self, pkt),
+                instructions=[
+                    ofp.instruction.apply_actions(actions)],
+                buffer_id=ofp.OFP_NO_BUFFER,
+                priority=1000)
+        self.controller.message_send(request)
+
+        do_barrier(self.controller)
+
+        logging.info("Sending packet, expecting output to port %d", out_port)
+        self.dataplane.send(in_port, str(pkt))
+        receive_pkt_check(self.dataplane, str(exp_pkt), [out_port],
+                          set(openflow_ports()) - set([out_port]), self)
+
+class PushVlan(BaseModifyPacketTest):
+    """
+    Push a vlan tag (vid=0, pcp=0)
+    """
+    def runTest(self):
+        actions = [ofp.action.push_vlan(ethertype=0x8100)]
+        pkt = simple_tcp_packet()
+        exp_pkt = simple_tcp_packet(dl_vlan_enable=True, pktlen=104)
+        self.verify_modify(actions, pkt, exp_pkt)
+
+class PopVlan(BaseModifyPacketTest):
+    """
+    Pop a vlan tag
+    """
+    def runTest(self):
+        actions = [ofp.action.pop_vlan()]
+        pkt = simple_tcp_packet(dl_vlan_enable=True, pktlen=104)
+        exp_pkt = simple_tcp_packet()
+        self.verify_modify(actions, pkt, exp_pkt)
+
+class SetVlanVid(BaseModifyPacketTest):
+    """
+    Set the vlan vid
+    """
+    def runTest(self):
+        actions = [ofp.action.set_field(ofp.oxm.vlan_vid(2))]
+        pkt = simple_tcp_packet(dl_vlan_enable=True, vlan_vid=1)
+        exp_pkt = simple_tcp_packet(dl_vlan_enable=True, vlan_vid=2)
+        self.verify_modify(actions, pkt, exp_pkt)
+
+class SetVlanPcp(BaseModifyPacketTest):
+    """
+    Set the vlan priority
+    """
+    def runTest(self):
+        actions = [ofp.action.set_field(ofp.oxm.vlan_pcp(2))]
+        pkt = simple_tcp_packet(dl_vlan_enable=True, vlan_pcp=1)
+        exp_pkt = simple_tcp_packet(dl_vlan_enable=True, vlan_pcp=2)
+        self.verify_modify(actions, pkt, exp_pkt)
diff --git a/tests-1.3/match.py b/tests-1.3/match.py
index c8bf51d..c807ee8 100644
--- a/tests-1.3/match.py
+++ b/tests-1.3/match.py
@@ -34,9 +34,7 @@
         dicts mapping from string names (used in log messages) to string
         packet data.
         """
-        ports = sorted(config["port_map"].keys())
-        in_port = ports[0]
-        out_port = ports[1]
+        in_port, out_port = openflow_ports(2)
 
         logging.info("Running match test for %s", match.show())
 
@@ -89,10 +87,7 @@
     Match on ingress port
     """
     def runTest(self):
-        ports = sorted(config["port_map"].keys())
-        in_port = ports[0]
-        out_port = ports[1]
-        bad_port = ports[2]
+        in_port, out_port, bad_port = openflow_ports(3)
 
         match = ofp.match([
             ofp.oxm.in_port(in_port)
diff --git a/tests/nicira_dec_ttl.py b/tests/nicira_dec_ttl.py
index b29f611..39b7f21 100644
--- a/tests/nicira_dec_ttl.py
+++ b/tests/nicira_dec_ttl.py
@@ -26,7 +26,7 @@
         outpkt = simple_tcp_packet(pktlen=100, ip_ttl=3)
         msg = ofp.message.packet_out(in_port=ofp.OFPP_NONE,
                                      data=str(outpkt),
-                                     buffer_id=x0xffffffff,
+                                     buffer_id=0xffffffff,
                                      actions=[
                                          ofp.action.nicira_dec_ttl(),
                                          ofp.action.output(port=portA),
@@ -39,3 +39,25 @@
         receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=2), [portA], [], self)
         receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=1), [portB], [], self)
         receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=0), [], [portC], self)
+
+@nonstandard
+class TtlDecrementZeroTtl(base_tests.SimpleDataPlane):
+    def runTest(self):
+        of_ports = config["port_map"].keys()
+        of_ports.sort()
+        self.assertTrue(len(of_ports) >= 2, "Not enough ports for test")
+        portA = of_ports[0]
+        portB = of_ports[1]
+
+        outpkt = simple_tcp_packet(pktlen=100, ip_ttl=0)
+        msg = ofp.message.packet_out(in_port=ofp.OFPP_NONE,
+                                     data=str(outpkt),
+                                     buffer_id=0xffffffff,
+                                     actions=[
+                                         ofp.action.output(port=portA),
+                                         ofp.action.nicira_dec_ttl(),
+                                         ofp.action.output(port=portB)])
+        self.controller.message_send(msg)
+
+        receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=0), [portA], [], self)
+        receive_pkt_check(self.dataplane, simple_tcp_packet(ip_ttl=0), [], [portB], self)