VOL-1308 - OpenOMCI decode failure for Serial Number in Circuit Pack(#6) and ONU-G(#256)

Added a new StrCompoundField that supports a composite aggregation of 2 fields to decode the serial number of OMCI that is 4 ascii and 4 binary digits. Representation is a 12 digit value: AAAAxxxxxxxx

Change-Id: I4934cae4746883263453f98080f5fd767d366631
diff --git a/tests/utests/voltha/extensions/omci/test_omci.py b/tests/utests/voltha/extensions/omci/test_omci.py
index de851c3..4270399 100644
--- a/tests/utests/voltha/extensions/omci/test_omci.py
+++ b/tests/utests/voltha/extensions/omci/test_omci.py
@@ -74,7 +74,7 @@
 
         e = CircuitPack(
             number_of_ports=4,
-            serial_number='123-123A',
+            serial_number='BCMX31323334', # serial number is 4 ascii + 4 hex. 8 octets on the wire
             version='a1c12fba91de',
             vendor_id='BCM',
             total_tcont_buffer_number=128
@@ -82,11 +82,11 @@
 
         # Full object
         self.assertEqual(e.serialize(),
-                         '\x04123-123Aa1c12fba91de\x00\x00BCM\x00\x80')
+                         '\x04BCMX1234a1c12fba91de\x00\x00BCM\x00\x80')
 
         # Explicit mask with valid values
         self.assertEqual(e.serialize(0x800), 'BCM\x00')
-        self.assertEqual(e.serialize(0x6800), '\x04123-123ABCM\x00')
+        self.assertEqual(e.serialize(0x6800), '\x04BCMX1234BCM\x00')
 
         # Referring to an unfilled field is regarded as error
         self.assertRaises(OmciUninitializedFieldError, e.serialize, 0xc00)
diff --git a/tests/utests/voltha/extensions/omci/test_omci_cc.py b/tests/utests/voltha/extensions/omci/test_omci_cc.py
index 102e017..de80614 100644
--- a/tests/utests/voltha/extensions/omci/test_omci_cc.py
+++ b/tests/utests/voltha/extensions/omci/test_omci_cc.py
@@ -655,6 +655,24 @@
 
         self.assertTrue(True, 'Truth is the truth')
 
+    def test_rx_decode_onu_g(self):
+        self.setup_one_of_each()
+
+        omci_cc = self.onu_handler.omci_cc
+        omci_cc.enabled = True
+        snapshot = self._snapshot_stats()
+
+        msg = '001e2e0a0002000001000000e000424657530000' \
+              '0000000000000000000000324246575300107496' \
+              '00000028e7fb4a91'
+
+        omci_cc.receive_message(hex2raw(msg))
+
+        # Note: No counter increments
+        self.assertEqual(omci_cc.rx_frames, snapshot['rx_frames'] + 1)
+        self.assertEqual(omci_cc.rx_unknown_me, snapshot['rx_unknown_me'])
+        self.assertEqual(omci_cc.rx_unknown_tid, snapshot['rx_unknown_tid'] + 1)
+        self.assertEqual(omci_cc.rx_onu_frames, snapshot['rx_onu_frames'])
 
 if __name__ == '__main__':
     main()
diff --git a/voltha/extensions/omci/omci_defs.py b/voltha/extensions/omci/omci_defs.py
index 5bd6841..a4f7363 100644
--- a/voltha/extensions/omci/omci_defs.py
+++ b/voltha/extensions/omci/omci_defs.py
@@ -14,9 +14,6 @@
 # limitations under the License.
 #
 from enum import Enum, IntEnum
-from scapy.fields import PadField
-from scapy.packet import Raw
-
 
 class OmciUninitializedFieldError(Exception):
     pass
@@ -25,23 +22,6 @@
 class OmciInvalidTypeError(Exception):
     pass
 
-
-class FixedLenField(PadField):
-    """
-    This Pad field limits parsing of its content to its size
-    """
-    def __init__(self, fld, align, padwith='\x00'):
-        super(FixedLenField, self).__init__(fld, align, padwith)
-
-    def getfield(self, pkt, s):
-        remain, val = self._fld.getfield(pkt, s[:self._align])
-        if isinstance(val.payload, Raw) and \
-                not val.payload.load.replace(self._padwith, ''):
-            # raw payload is just padding
-            val.remove_payload()
-        return remain + s[self._align:], val
-
-
 def bitpos_from_mask(mask, lsb_pos=0, increment=1):
     """
     Turn a decimal value (bitmask) into a list of indices where each
diff --git a/voltha/extensions/omci/omci_entities.py b/voltha/extensions/omci/omci_entities.py
index bf47f8e..b523ebf 100644
--- a/voltha/extensions/omci/omci_entities.py
+++ b/voltha/extensions/omci/omci_entities.py
@@ -25,9 +25,9 @@
 
 from voltha.extensions.omci.omci_defs import OmciUninitializedFieldError, \
     AttributeAccess, OmciNullPointer, EntityOperations, OmciInvalidTypeError
+from voltha.extensions.omci.omci_fields import OmciSerialNumberField
 from voltha.extensions.omci.omci_defs import bitpos_from_mask
 
-
 class EntityClassAttribute(object):
 
     def __init__(self, fld, access=set(), optional=False, range_check=None,
@@ -276,7 +276,7 @@
             range_check=lambda x: 0 <= x < 255 or 256 <= x < 511),
         ECA(ByteField("type", None), {AA.R, AA.SBC}),
         ECA(ByteField("number_of_ports", None), {AA.R}, optional=True),
-        ECA(StrFixedLenField("serial_number", None, 8), {AA.R}),
+        ECA(OmciSerialNumberField("serial_number"), {AA.R}),
         ECA(StrFixedLenField("version", None, 14), {AA.R}),
         ECA(StrFixedLenField("vendor_id", None, 4), {AA.R}),
         ECA(ByteField("administrative_state", None), {AA.R, AA.W}),
@@ -648,7 +648,7 @@
             range_check=lambda x: x == 0),
         ECA(StrFixedLenField("vendor_id", None, 4), {AA.R}),
         ECA(StrFixedLenField("version", None, 14), {AA.R}),
-        ECA(StrFixedLenField("serial_number", None, 8), {AA.R}),
+        ECA(OmciSerialNumberField("serial_number"), {AA.R}),
         ECA(ByteField("traffic_management_options", None), {AA.R},
             range_check=lambda x: 0 <= x <= 2),
         ECA(ByteField("vp_vc_cross_connection_option", 0), {AA.R},
diff --git a/voltha/extensions/omci/omci_fields.py b/voltha/extensions/omci/omci_fields.py
new file mode 100644
index 0000000..a6df815
--- /dev/null
+++ b/voltha/extensions/omci/omci_fields.py
@@ -0,0 +1,80 @@
+#
+# Copyright 2017 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# 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 License for the specific language governing permissions and
+# limitations under the License.
+#
+import binascii
+from scapy.fields import Field, StrFixedLenField, PadField
+from scapy.packet import Raw
+
+class FixedLenField(PadField):
+    """
+    This Pad field limits parsing of its content to its size
+    """
+    def __init__(self, fld, align, padwith='\x00'):
+        super(FixedLenField, self).__init__(fld, align, padwith)
+
+    def getfield(self, pkt, s):
+        remain, val = self._fld.getfield(pkt, s[:self._align])
+        if isinstance(val.payload, Raw) and \
+                not val.payload.load.replace(self._padwith, ''):
+            # raw payload is just padding
+            val.remove_payload()
+        return remain + s[self._align:], val
+
+class StrCompoundField(Field):
+    __slots__ = ['flds']
+
+    def __init__(self, name, flds):
+        super(StrCompoundField, self).__init__(name=name, default=None, fmt='s')
+        self.flds = flds
+        for fld in self.flds:
+            assert not fld.holds_packets, 'compound field cannot have packet field members'
+
+    def addfield(self, pkt, s, val):
+        for fld in self.flds:
+            # run though fake add/get to consume the relevant portion of the input value for this field
+            x, extracted = fld.getfield(pkt, fld.addfield(pkt, '', val))
+            l = len(extracted)
+            s = fld.addfield(pkt, s, val[0:l])
+            val = val[l:]
+        return s;
+
+    def getfield(self, pkt, s):
+        data = ''
+        for fld in self.flds:
+            s, value = fld.getfield(pkt, s)
+            if not isinstance(value, str):
+                value = fld.i2repr(pkt, value)
+            data += value
+        return s, data
+
+class XStrFixedLenField(StrFixedLenField):
+    """
+    XStrFixedLenField which value is printed as hexadecimal.
+    """
+    def i2m(self, pkt, x):
+        l = self.length_from(pkt) * 2
+        return None if x is None else binascii.a2b_hex(x)[0:l+1]
+
+    def m2i(self, pkt, x):
+        return None if x is None else binascii.b2a_hex(x)
+
+class OmciSerialNumberField(StrCompoundField):
+    def __init__(self, name, default=None):
+        assert default is None or (isinstance(default, str) and len(default) == 12), 'invalid default serial number'
+        vendor_default = default[0:4] if default is not None else None
+        vendor_serial_default = default[4:12] if default is not None else None
+        super(OmciSerialNumberField, self).__init__(name,
+            [StrFixedLenField('vendor_id', vendor_default, 4),
+            XStrFixedLenField('vendor_serial_number', vendor_serial_default, 4)])
diff --git a/voltha/extensions/omci/omci_frame.py b/voltha/extensions/omci/omci_frame.py
index 98faf42..05be99f 100644
--- a/voltha/extensions/omci/omci_frame.py
+++ b/voltha/extensions/omci/omci_frame.py
@@ -17,7 +17,7 @@
 from scapy.fields import ShortField, ConditionalField
 from scapy.packet import Packet
 
-from voltha.extensions.omci.omci_defs import FixedLenField
+from voltha.extensions.omci.omci_fields import FixedLenField
 from voltha.extensions.omci.omci_messages import OmciCreate, OmciDelete, \
     OmciDeleteResponse, OmciSet, OmciSetResponse, OmciGet, OmciGetResponse, \
     OmciGetAllAlarms, OmciGetAllAlarmsResponse, OmciGetAllAlarmsNext, \