Merge pull request #322 from rlane/of14-pyloxi

[of14] Add OF 1.4 support to the python backend
diff --git a/Makefile b/Makefile
index 018db0b..5f3d8e5 100644
--- a/Makefile
+++ b/Makefile
@@ -131,6 +131,7 @@
 	PYTHONPATH=${LOXI_OUTPUT_DIR}/pyloxi:. python py_gen/tests/of11.py
 	PYTHONPATH=${LOXI_OUTPUT_DIR}/pyloxi:. python py_gen/tests/of12.py
 	PYTHONPATH=${LOXI_OUTPUT_DIR}/pyloxi:. python py_gen/tests/of13.py
+	PYTHONPATH=${LOXI_OUTPUT_DIR}/pyloxi:. python py_gen/tests/of14.py
 
 check-c: c
 	make -j4 -C ${LOXI_OUTPUT_DIR}/locitest
diff --git a/cmdline.py b/cmdline.py
index 6395fff..de7070a 100644
--- a/cmdline.py
+++ b/cmdline.py
@@ -33,7 +33,7 @@
 # The default configuration dictionary for LOXI code generation
 options_default = {
     "lang"               : "c",
-    "version-list"       : "1.0 1.1 1.2 1.3",
+    "version-list"       : "1.0 1.1 1.2 1.3 1.4",
     "install-dir"        : "loxi_output",
 }
 
diff --git a/openflow_input/standard-1.4 b/openflow_input/standard-1.4
index d7e38eb..918078c 100644
--- a/openflow_input/standard-1.4
+++ b/openflow_input/standard-1.4
@@ -772,6 +772,18 @@
     uint16_t length; /* Length in bytes of this property. */
 };
 
+struct of_port_desc_prop_ethernet : of_port_desc_prop {
+    uint16_t type == 0;
+    uint16_t length;
+    pad(4);
+    uint32_t curr;
+    uint32_t advertised;
+    uint32_t supported;
+    uint32_t peer;
+    uint32_t curr_speed;
+    uint32_t max_speed;
+};
+
 struct of_port_desc {
     of_port_no_t port_no;
     uint16_t length;
@@ -834,8 +846,7 @@
     list(of_port_mod_prop_t) properties;
 };
 
-// FIXME Does this need to be v4?
-struct of_match_v4(align=8, length_includes_align=False) {
+struct of_match_v3(align=8, length_includes_align=False) {
     uint16_t type == 1;
     uint16_t length;
     list(of_oxm_t) oxm_list;
@@ -979,7 +990,8 @@
 struct of_instruction_goto_table : of_instruction {
     uint16_t type == 1;
     uint16_t len;
-    of_octets_t exp_data;
+    uint8_t table_id;
+    pad(3);
 };
 
 struct of_instruction_write_metadata : of_instruction {
@@ -1534,6 +1546,8 @@
     uint16_t length;
     pad(2);
     of_port_no_t port_no;
+    uint32_t duration_sec;
+    uint32_t duration_nsec;
     uint64_t rx_packets;
     uint64_t tx_packets;
     uint64_t rx_bytes;
@@ -1542,8 +1556,6 @@
     uint64_t tx_dropped;
     uint64_t rx_errors;
     uint64_t tx_errors;
-    uint32_t duration_sec;
-    uint32_t duration_nsec;
     list(of_port_stats_prop_t) properties;
 };
 
diff --git a/py_gen/codegen.py b/py_gen/codegen.py
index 3b0988e..a664d87 100644
--- a/py_gen/codegen.py
+++ b/py_gen/codegen.py
@@ -43,6 +43,12 @@
     'of_instruction_id': 'instruction_id',
     'of_meter_band': 'meter_band',
     'of_bsn_tlv': 'bsn_tlv',
+    'of_port_desc_prop': 'port_desc_prop',
+    'of_port_stats_prop': 'port_stats_prop',
+    'of_port_mod_prop': 'port_mod_prop',
+    'of_table_mod_prop': 'table_mod_prop',
+    'of_queue_desc_prop': 'queue_desc_prop',
+    'of_queue_stats_prop': 'queue_stats_prop',
 }
 
 # Return the module and class names for the generated Python class
diff --git a/py_gen/templates/_common_extra.py b/py_gen/templates/_common_extra.py
index 8639dc4..f801965 100644
--- a/py_gen/templates/_common_extra.py
+++ b/py_gen/templates/_common_extra.py
@@ -35,4 +35,7 @@
 :: elif version == OFVersions.VERSION_1_3:
 :: # HACK
 match = match_v3
+:: elif version == OFVersions.VERSION_1_4:
+:: # HACK
+match = match_v3
 :: #endif
diff --git a/py_gen/templates/toplevel_init.py b/py_gen/templates/toplevel_init.py
index e5493a5..0393169 100644
--- a/py_gen/templates/toplevel_init.py
+++ b/py_gen/templates/toplevel_init.py
@@ -39,20 +39,13 @@
     """
     Import and return the protocol module for the given wire version.
     """
-    if ver == 1:
-        import of10
-        return of10
-    elif ver == 2:
-        import of11
-        return of11
-    elif ver == 3:
-        import of12
-        return of12
-    elif ver == 4:
-        import of13
-        return of13
-    else:
-        raise ValueError
+:: for v in loxi_globals.OFVersions.all_supported:
+    if ver == ${v.wire_version}:
+        import of${v.version.replace('.', '')}
+        return of${v.version.replace('.', '')}
+
+:: #endfor
+    raise ValueError
 
 class ProtocolError(Exception):
     """
diff --git a/py_gen/tests/of14.py b/py_gen/tests/of14.py
new file mode 100644
index 0000000..3fdfb8e
--- /dev/null
+++ b/py_gen/tests/of14.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+# Copyright 2014, Big Switch Networks, Inc.
+#
+# LoxiGen is licensed under the Eclipse Public License, version 1.0 (EPL), with
+# the following special exception:
+#
+# LOXI Exception
+#
+# As a special exception to the terms of the EPL, you may distribute libraries
+# generated by LoxiGen (LoxiGen Libraries) under the terms of your choice, provided
+# that copyright and licensing notices generated by LoxiGen are not altered or removed
+# from the LoxiGen Libraries and the notice provided below is (i) included in
+# the LoxiGen Libraries, if distributed in source code form and (ii) included in any
+# documentation for the LoxiGen Libraries, if distributed in binary form.
+#
+# Notice: "Copyright 2014, Big Switch Networks, Inc. This library was generated by the LoxiGen Compiler."
+#
+# You may not use this file except in compliance with the EPL or LOXI Exception. You may obtain
+# a copy of the EPL at:
+#
+# http://www.eclipse.org/legal/epl-v10.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# EPL for the specific language governing permissions and limitations
+# under the EPL.
+import unittest
+from testutil import test_serialization
+from testutil import add_datafiles_tests
+
+try:
+    import loxi
+    import loxi.of14 as ofp
+    from loxi.generic_util import OFReader
+except ImportError:
+    exit("loxi package not found. Try setting PYTHONPATH.")
+
+class TestImports(unittest.TestCase):
+    def test_toplevel(self):
+        import loxi
+        self.assertTrue(hasattr(loxi, "ProtocolError"))
+        self.assertEquals(loxi.version_names[5], "1.4")
+        ofp = loxi.protocol(5)
+        self.assertEquals(ofp.OFP_VERSION, 5)
+        self.assertTrue(hasattr(ofp, "action"))
+        self.assertTrue(hasattr(ofp, "common"))
+        self.assertTrue(hasattr(ofp, "const"))
+        self.assertTrue(hasattr(ofp, "message"))
+        self.assertTrue(hasattr(ofp, "oxm"))
+
+    def test_version(self):
+        import loxi
+        self.assertTrue(hasattr(loxi.of14, "ProtocolError"))
+        self.assertTrue(hasattr(loxi.of14, "OFP_VERSION"))
+        self.assertEquals(loxi.of14.OFP_VERSION, 5)
+        self.assertTrue(hasattr(loxi.of14, "action"))
+        self.assertTrue(hasattr(loxi.of14, "common"))
+        self.assertTrue(hasattr(loxi.of14, "const"))
+        self.assertTrue(hasattr(loxi.of14, "message"))
+        self.assertTrue(hasattr(loxi.of14, "oxm"))
+
+# The majority of the serialization tests are created here using the files in
+# the test_data directory.
+class TestDataFiles(unittest.TestCase):
+    pass
+add_datafiles_tests(TestDataFiles, 'of14/', ofp)
+
+class TestAllof14(unittest.TestCase):
+    """
+    Round-trips every class through serialization/deserialization.
+    Not a replacement for handcoded tests because it only uses the
+    default member values.
+    """
+
+    def setUp(self):
+        mods = [ofp.action,ofp.message,ofp.common,ofp.oxm]
+        self.klasses = [klass for mod in mods
+                              for klass in mod.__dict__.values()
+                              if isinstance(klass, type) and
+                                 issubclass(klass, loxi.OFObject) and
+                                 not hasattr(klass, 'subtypes')]
+        self.klasses.sort(key=lambda x: str(x))
+
+    def test_serialization(self):
+        expected_failures = [
+            ofp.action.set_field, # field defaults to None
+        ]
+        for klass in self.klasses:
+            def fn():
+                obj = klass()
+                if hasattr(obj, "xid"): obj.xid = 42
+                buf = obj.pack()
+                obj2 = klass.unpack(OFReader(buf))
+                self.assertEquals(obj, obj2)
+            if klass in expected_failures:
+                self.assertRaises(Exception, fn)
+            else:
+                fn()
+
+    def test_parse_message(self):
+        expected_failures = [
+        ]
+        for klass in self.klasses:
+            if not issubclass(klass, ofp.message.message):
+                continue
+            def fn():
+                obj = klass(xid=42)
+                buf = obj.pack()
+                obj2 = ofp.message.parse_message(buf)
+                self.assertEquals(obj, obj2)
+            if klass in expected_failures:
+                self.assertRaises(Exception, fn)
+            else:
+                fn()
+
+    def test_show(self):
+        expected_failures = []
+        for klass in self.klasses:
+            def fn():
+                obj = klass()
+                if hasattr(obj, "xid"): obj.xid = 42
+                obj.show()
+            if klass in expected_failures:
+                self.assertRaises(Exception, fn)
+            else:
+                fn()
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test_data/of14/flow_add.data b/test_data/of14/flow_add.data
new file mode 100644
index 0000000..88cf939
--- /dev/null
+++ b/test_data/of14/flow_add.data
@@ -0,0 +1,104 @@
+-- binary
+05 0e # version, type
+00 80 # length
+12 34 56 78 # xid
+
+fe dc ba 98 76 54 32 10 # cookie
+
+ff 00 ff 00 ff 00 ff 00 # cookie_mask
+
+03 # table_id
+00 # _command
+00 05 # idle_timeout
+00 0a # hard_timeout
+17 70 # priority
+
+00 00 00 32 # buffer_id
+00 00 00 06 # out_port
+
+00 00 00 08 # out_group
+00 00 # flags
+aa bb # importance
+
+00 01 # match.type
+00 3F # match.length # 59 bytes OXMs + 4 bytes match header
+
+80 00 01 08 # match.oxm_list[0].type_len - IN_PORT
+00 00 00 04 # match.oxm_list[0].value
+00 00 00 05 # match.oxm_list[0].mask
+
+80 00 0A 02 # match.oxm_list[1].type_len - ETH_TYPE
+86 DD # match.oxm_list[1].value - ETH_TYPE = IPv6
+
+80 00 14 01 # match.oxm_list[2].type_len - IP Proto
+06 # match.oxm_list[2].value = IP_PROTO = TCP
+
+80 00 35 20 # match.oxm_list[3].type_len - IPV6_SRC
+1C CA FE 1C B1 10 1C 00 00 28 00 00 00 00 00 00 # match.oxm_list[3].value
+FF FF FF FF FF F0 FF FF 1C 2C 3C 00 00 00 00 00 # match.oxm_list[3].mask
+
+00 # match.pad
+
+00 01 # instructions[0].type
+00 08 # instructions[0].length
+04 # instructions[0].table_id
+00 00 00 # pad
+
+00 01 # instructions[1].type
+00 08 # instructions[1].length
+07 # instructions[1].table_id
+00 00 00 # pad
+-- python
+ofp.message.flow_add(
+    xid=0x12345678,
+    cookie=0xFEDCBA9876543210,
+    cookie_mask=0xFF00FF00FF00FF00,
+    table_id=3,
+    idle_timeout=5,
+    hard_timeout=10,
+    priority=6000,
+    buffer_id=50,
+    out_port=6,
+    out_group=8,
+    flags=0,
+    importance=0xaabb,
+    match=ofp.match(oxm_list=[
+        ofp.oxm.in_port_masked(value=4, value_mask=5),
+        ofp.oxm.eth_type(value=0x86dd),
+        ofp.oxm.ip_proto(value=6),
+        ofp.oxm.ipv6_src_masked(
+            value     ='\x1C\xCA\xFE\x1C\xB1\x10\x1C\x00\x00\x28\x00\x00\x00\x00\x00\x00',
+            value_mask='\xFF\xFF\xFF\xFF\xFF\xF0\xFF\xFF\x1C\x2C\x3C\x00\x00\x00\x00\x00')
+        ]),
+    instructions=[
+        ofp.instruction.goto_table(table_id=4),
+        ofp.instruction.goto_table(table_id=7)])
+-- java
+builder.setXid(0x12345678)
+    .setCookie(U64.parseHex("FEDCBA9876543210"))
+    .setCookieMask(U64.parseHex("FF00FF00FF00FF00"))
+    .setTableId(TableId.of(3))
+    .setIdleTimeout(5)
+    .setHardTimeout(10)
+    .setPriority(6000)
+    .setBufferId(OFBufferId.of(50))
+    .setOutPort(OFPort.of(6))
+    .setOutGroup(OFGroup.of(8))
+    .setFlags(ImmutableSet.<OFFlowModFlags>of())
+    .setImportance(0xaabb)
+    .setMatch(
+        factory.buildMatch()
+            .setMasked(MatchField.IN_PORT, OFPort.of(4), OFPort.of(5))
+            .setExact(MatchField.ETH_TYPE, EthType.IPv6)
+            .setExact(MatchField.IP_PROTO, IpProtocol.TCP)
+            .setMasked(MatchField.IPV6_SRC, 
+                       IPv6Address.of(0x1CCAFE1CB1101C00l, 0x0028000000000000l),
+                       IPv6Address.of(0xFFFFFFFFFFF0FFFFl, 0x1C2C3C0000000000l))
+        	.build()
+    )
+    .setInstructions(
+        ImmutableList.<OFInstruction>of(
+                factory.instructions().gotoTable(TableId.of(4)),
+                factory.instructions().gotoTable(TableId.of(7))
+        )
+    );
diff --git a/test_data/of14/port_desc_stats_reply.data b/test_data/of14/port_desc_stats_reply.data
new file mode 100644
index 0000000..b969b4a
--- /dev/null
+++ b/test_data/of14/port_desc_stats_reply.data
@@ -0,0 +1,38 @@
+-- binary
+05 13 # version/type
+00 58 # length
+00 00 00 05 # xid
+00 0d # stats_type
+00 00 # flags
+00 00 00 00 # pad
+00 00 00 01 # entries[0].port_no
+00 48 # entries[0].length
+00 00 # pad
+01 02 03 04 05 06 # entries[0].hw_addr
+00 00 # pad
+69 6e 74 65 72 66 61 63 65 31 32 33 34 35 36 37 # entries[0].name
+00 00 00 50 # entries[0].config
+00 00 00 60 # entries[0].state
+00 00 # entries[0].properties[0].type (ethernet)
+00 20 # entries[0].properties[0].length 
+00 00 00 00 # pad
+00 00 00 70 # entries[0].properties[0].curr
+00 00 00 00 # entries[0].properties[0].advertised
+00 00 00 00 # entries[0].properties[0].supported
+00 00 00 00 # entries[0].properties[0].peer
+00 00 00 00 # entries[0].properties[0].curr_speed
+00 00 00 80 # entries[0].properties[0].max_speed
+-- python
+ofp.message.port_desc_stats_reply(
+    xid=5, flags=0, entries=[
+        ofp.port_desc(
+            port_no=1,
+            hw_addr=[1,2,3,4,5,6],
+            name="interface1234567",
+            config=0x50,
+            state=0x60,
+            properties=[
+                ofp.port_desc_prop.ethernet(
+                    curr=0x70,
+                    max_speed=0x80)])])
+
diff --git a/test_data/of14/port_stats_reply.data b/test_data/of14/port_stats_reply.data
new file mode 100644
index 0000000..43dc471
--- /dev/null
+++ b/test_data/of14/port_stats_reply.data
@@ -0,0 +1,88 @@
+-- binary
+05 13 # version/type
+00 d8 # length
+00 00 00 05 # xid
+00 04 # stats_type
+00 00 # flags
+00 00 00 00 # pad
+00 78 # entries[0].length
+00 00 # pad
+00 00 00 01 # entries[0].port_no
+00 00 00 00 # entries[0].duration_sec
+00 00 00 00 # entries[0].duration_nsec
+00 00 00 00 00 00 00 01 # entries[0].rx_packets
+00 00 00 00 00 00 00 00 # entries[0].tx_packets
+00 00 00 00 00 00 00 00 # entries[0].rx_bytes
+00 00 00 00 00 00 00 00 # entries[0].tx_bytes
+00 00 00 00 00 00 00 00 # entries[0].rx_dropped
+00 00 00 00 00 00 00 00 # entries[0].tx_dropped
+00 00 00 00 00 00 00 00 # entries[0].rx_errors
+00 00 00 00 00 00 00 02 # entries[0].tx_errors
+00 00 # entries[0].properties[0].type (ethernet)
+00 28 # entries[0].properties[0].length 
+00 00 00 00 # pad
+00 00 00 00 00 00 00 01 # entries[0].properties[0].rx_frame_err
+00 00 00 00 00 00 00 02 # entries[0].properties[0].rx_over_err
+00 00 00 00 00 00 00 03 # entries[0].properties[0].rx_crc_err
+00 00 00 00 00 00 00 04 # entries[0].properties[0].collisions
+00 50 # entries[1].length
+00 00 # pad
+ff ff ff fe # entries[1].port_no
+00 00 00 00 # entries[1].duration_sec
+00 00 00 00 # entries[1].duration_nsec
+00 00 00 00 00 00 00 03 # entries[1].rx_packets
+00 00 00 00 00 00 00 00 # entries[1].tx_packets
+00 00 00 00 00 00 00 00 # entries[1].rx_bytes
+00 00 00 00 00 00 00 00 # entries[1].tx_bytes
+00 00 00 00 00 00 00 00 # entries[1].rx_dropped
+00 00 00 00 00 00 00 00 # entries[1].tx_dropped
+00 00 00 00 00 00 00 00 # entries[1].rx_errors
+00 00 00 00 00 00 00 04 # entries[1].tx_errors
+-- python
+ofp.message.port_stats_reply(
+    xid=5, flags=0, entries=[
+        ofp.port_stats_entry(port_no=1, rx_packets=1, tx_errors=2,
+            properties=[
+                ofp.port_stats_prop.ethernet(
+                    rx_frame_err=1,
+                    rx_over_err=2,
+                    rx_crc_err=3,
+                    collisions=4)]),
+        ofp.port_stats_entry(port_no=ofp.OFPP_LOCAL, rx_packets=3, tx_errors=4)])
+-- c
+obj = of_port_stats_reply_new(OF_VERSION_1_4);
+{
+    of_object_t list;
+    of_port_stats_reply_entries_bind(obj, &list);
+    {
+        of_object_t *obj = of_port_stats_entry_new(OF_VERSION_1_4);
+        of_port_stats_entry_port_no_set(obj, 1);
+        of_port_stats_entry_rx_packets_set(obj, 1);
+        of_port_stats_entry_tx_packets_set(obj, 0);
+        of_port_stats_entry_rx_bytes_set(obj, 0);
+        of_port_stats_entry_tx_bytes_set(obj, 0);
+        of_port_stats_entry_rx_dropped_set(obj, 0);
+        of_port_stats_entry_tx_dropped_set(obj, 0);
+        of_port_stats_entry_rx_errors_set(obj, 0);
+        of_port_stats_entry_tx_errors_set(obj, 2);
+        /* TODO append property */
+        of_list_append(&list, obj);
+        of_object_delete(obj);
+    }
+    {
+        of_object_t *obj = of_port_stats_entry_new(OF_VERSION_1_4);
+        of_port_stats_entry_port_no_set(obj, OF_PORT_DEST_LOCAL);
+        of_port_stats_entry_rx_packets_set(obj, 3);
+        of_port_stats_entry_tx_packets_set(obj, 0);
+        of_port_stats_entry_rx_bytes_set(obj, 0);
+        of_port_stats_entry_tx_bytes_set(obj, 0);
+        of_port_stats_entry_rx_dropped_set(obj, 0);
+        of_port_stats_entry_tx_dropped_set(obj, 0);
+        of_port_stats_entry_rx_errors_set(obj, 0);
+        of_port_stats_entry_tx_errors_set(obj, 4);
+        of_list_append(&list, obj);
+        of_object_delete(obj);
+    }
+}
+of_port_stats_reply_flags_set(obj, 0);
+of_port_stats_reply_xid_set(obj, 5);