Repository: kytos/python-openflow

Issue:
Implement VLAN Class as Network Types
@jab1982 it will start this implementation, but @renanrodrigo please feel free to start this.

Comments:

Patch:
diff --git a/pyof/foundation/constants.py b/pyof/foundation/constants.py
index 966305c..b799b0e 100644
--- a/pyof/foundation/constants.py
+++ b/pyof/foundation/constants.py
@@ -11,3 +11,5 @@ OFP_MAX_PORT_NAME_LEN = 16
 OFP_MAX_TABLE_NAME_LEN = 32
 SERIAL_NUM_LEN = 32
 DESC_STR_LEN = 256
+
+VLAN_TPID = 33024
diff --git a/pyof/foundation/network_types.py b/pyof/foundation/network_types.py
index 9c31fb4..1158903 100644
--- a/pyof/foundation/network_types.py
+++ b/pyof/foundation/network_types.py
@@ -6,9 +6,98 @@ Defines and Implements Basic Network packet types , such as Ethertnet and LLDP.
 from pyof.foundation.base import GenericStruct
 from pyof.foundation.basic_types import (
     BinaryData, HWAddress, IPAddress, UBInt8, UBInt16)
-from pyof.foundation.exceptions import PackException
+from pyof.foundation.constants import VLAN_TPID
+from pyof.foundation.exceptions import PackException, UnpackException
 
-__all__ = ('Ethernet', 'GenericTLV', 'IPv4', 'TLVWithSubType', 'LLDP')
+__all__ = ('Ethernet', 'GenericTLV', 'IPv4', 'VLAN', 'TLVWithSubType', 'LLDP')
+
+
+class VLAN(GenericStruct):
+    """802.1q VLAN header."""
+
+    #: tpid (:class:`UBInt16`): Tag Protocol Identifier
+    tpid = UBInt16(VLAN_TPID)
+    #: _tci (:class:`UBInt16`): Tag Control Information - has the
+    #: Priority Code Point, DEI/CFI bit and the VLAN ID
+    _tci = UBInt16()
+
+    def __init__(self, pcp=None, cfi=None, vid=None):
+        """The constructor receives the parameters below.
+
+        If no arguments are set for a particular instance, it is interpreted as
+        abscence of VLAN information, and the pack() method will return an
+        empty binary string.
+
+        Args:
+            tpid (int): Tag Protocol Identifier. Defaults to 0x8100 for 802.1q.
+            pcp (int): 802.1p Priority Code Point. Defaults to 0 for Best
+                       Effort Queue.
+            cfi (int): Canonical Format Indicator. Defaults to 0 for Ethernet.
+            vid (int): VLAN ID. If no VLAN is specified, value is 0.
+        """
+        super().__init__()
+        self.tpid = VLAN_TPID
+        self.pcp = pcp
+        self.cfi = cfi
+        self.vid = vid
+
+    def pack(self, value=None):
+        """Pack the struct in a binary representation.
+
+        Merge some fields to ensure correct packing.
+
+        If no arguments are set for a particular instance, it is interpreted as
+        abscence of VLAN information, and the pack() method will return an
+        empty binary string.
+
+        Returns:
+            bytes: Binary representation of this instance.
+        """
+        if isinstance(value, type(self)):
+            return value.pack()
+
+        if self.pcp is None and self.cfi is None and self.vid is None:
+            return b''
+        self.pcp = self.pcp if self.pcp is not None else 0
+        self.cfi = self.cfi if self.cfi is not None else 0
+        self.vid = self.vid if self.vid is not None else 0
+        self._tci = self.pcp << 13 | self.cfi << 12 | self.vid
+        return super().pack()
+
+    def _validate(self):
+        """Assure this is a 802.1q VLAN header instance."""
+        if self.tpid.value != VLAN_TPID:
+            raise UnpackException
+        return
+
+    def unpack(self, buff, offset=0):
+        """Unpack a binary struct into this object's attributes.
+
+        Return the values instead of the lib's basic types.
+
+        After unpacking, the abscence of a `tpid` value causes the assignment
+        of None to the field values to indicate that there is no VLAN
+        information.
+
+        Args:
+            buff (bytes): Binary buffer.
+            offset (int): Where to begin unpacking.
+
+        Raises:
+            :exc:`~.exceptions.UnpackException`: If unpack fails.
+        """
+        super().unpack(buff, offset)
+        if self.tpid.value:
+            self._validate()
+            self.tpid = self.tpid.value
+            self.pcp = self._tci.value >> 13
+            self.cfi = (self._tci.value >> 12) & 1
+            self.vid = self._tci.value & 4095
+        else:
+            self.tpid = VLAN_TPID
+            self.pcp = None
+            self.cfi = None
+            self.vid = None
 
 
 class Ethernet(GenericStruct):
@@ -30,11 +119,12 @@ class Ethernet(GenericStruct):
 
     destination = HWAddress()
     source = HWAddress()
+    vlan = VLAN()
     ether_type = UBInt16()
     data = BinaryData()
 
-    def __init__(self, destination=None, source=None, ether_type=None,
-                 data=b''):
+    def __init__(self, destination=None, source=None, vlan=VLAN(),
+                 ether_type=None, data=b''):
         """Create an instance and set its attributes.
 
         Args:
@@ -50,6 +140,7 @@ class Ethernet(GenericStruct):
         super().__init__()
         self.destination = destination
         self.source = source
+        self.vlan = vlan
         self.ether_type = ether_type
         self.data = data
 
@@ -61,6 +152,31 @@ class Ethernet(GenericStruct):
         """
         return hash(self.pack())
 
+    def unpack(self, buff, offset=0):
+        """Unpack a binary message into this object's attributes.
+
+        Unpack the binary value *buff* and update this object attributes based
+        on the results.
+
+        Ethernet headers may have VLAN tags. If no VLAN tag is found, a
+        'wildcard VLAN tag' is inserted to assure correct unpacking.
+
+        Args:
+            buff (bytes): Binary data package to be unpacked.
+            offset (int): Where to begin unpacking.
+
+        Raises:
+            UnpackException: If there is a struct unpacking error.
+        """
+        # Checking if the EtherType bytes are actually equal to VLAN_TPID -
+        # indicating that the packet is tagged. If it is not, we insert the
+        # equivalent to 'NULL VLAN data' (\x00\x00\x00\x00) to enable the
+        # correct unpacking process.
+        if buff[12:16] != VLAN_TPID.to_bytes(2, 'big'):
+            buff = buff[0:12] + b'\x00\x00\x00\x00' + buff[12:]
+
+        super().unpack(buff, offset)
+
 
 class GenericTLV(GenericStruct):
     """TLV structure of LLDP packets.
@@ -217,7 +333,7 @@ class IPv4(GenericStruct):
                  identification=0, flags=0, offset=0, ttl=255, protocol=0,
                  checksum=0, source="0.0.0.0", destination="0.0.0.0",
                  options=b'', data=b''):
-        """The contructor receives the parameters below.
+        """The constructor receives the parameters below.
 
         Args:
             version (int): IP protocol version. Defaults to 4.
diff --git a/pyof/v0x01/common/action.py b/pyof/v0x01/common/action.py
index 7041dfd..9e595e4 100644
--- a/pyof/v0x01/common/action.py
+++ b/pyof/v0x01/common/action.py
@@ -10,9 +10,9 @@ from pyof.foundation.constants import UBINT16_MAX_VALUE
 
 # Third-party imports
 
-__all__ = ('ActionType', 'ActionHeader', 'ActionOutput', 'ActionEnqueue',
-           'ActionVlanVid', 'ActionVlanPCP', 'ActionDLAddr', 'ActionNWAddr',
-           'ActionNWTos', 'ActionTPPort', 'ActionVendorHeader',
+__all__ = ('ActionType', 'ActionHeader', 'ActionOutput', 'ActionStripVlan',
+           'ActionEnqueue', 'ActionVlanVid', 'ActionVlanPCP', 'ActionDLAddr',
+           'ActionNWAddr', 'ActionNWTos', 'ActionTPPort', 'ActionVendorHeader',
            'ListOfActions')
 
 # Enums


Test patch:
diff --git a/tests/test_foundation/test_network_types.py b/tests/test_foundation/test_network_types.py
index a72fd42..ce7b910 100644
--- a/tests/test_foundation/test_network_types.py
+++ b/tests/test_foundation/test_network_types.py
@@ -2,7 +2,8 @@
 import unittest
 
 from pyof.foundation.basic_types import BinaryData
-from pyof.foundation.network_types import GenericTLV, IPv4
+from pyof.foundation.exceptions import UnpackException
+from pyof.foundation.network_types import VLAN, Ethernet, GenericTLV, IPv4
 
 
 class TestNetworkTypes(unittest.TestCase):
@@ -17,6 +18,79 @@ class TestNetworkTypes(unittest.TestCase):
         self.assertEqual(tlv.value.value, tlv_unpacked.value.value)
 
 
+class TestEthernet(unittest.TestCase):
+    """Test Ethernet frames."""
+
+    def test_Ethernet_pack(self):
+        """Test pack method of Ethernet class without VLAN tag."""
+        ethernet = Ethernet(destination='00:1f:3a:3e:9a:cf',
+                            source='00:15:af:d5:38:98', ether_type=0x800,
+                            data=b'testdata')
+        packed = ethernet.pack()
+        expected = b'\x00\x1f:>\x9a\xcf\x00\x15\xaf\xd58\x98\x08\x00testdata'
+        self.assertEqual(packed, expected)
+
+    def test_Ethernet_unpack(self):
+        """Test pack method of Ethernet class without VLAN tag."""
+        raw = b'\x00\x15\xaf\xd58\x98\x00\x1f:>\x9a\xcf\x08\x00testdata'
+        expected = Ethernet(destination='00:15:af:d5:38:98',
+                            source='00:1f:3a:3e:9a:cf', ether_type=0x800,
+                            data=b'testdata')
+        expected.pack()
+        unpacked = Ethernet()
+        unpacked.unpack(raw)
+        self.assertEqual(unpacked, expected)
+
+    def test_Tagged_Ethernet_pack(self):
+        """Test pack method of Ethernet class including VLAN tag."""
+        ethernet = Ethernet(destination='00:1f:3a:3e:9a:cf',
+                            source='00:15:af:d5:38:98', vlan=VLAN(vid=200),
+                            ether_type=0x800, data=b'testdata')
+        packed = ethernet.pack()
+        expected = b'\x00\x1f:>\x9a\xcf\x00\x15\xaf\xd58'
+        expected += b'\x98\x81\x00\x00\xc8\x08\x00testdata'
+        self.assertEqual(packed, expected)
+
+    def test_Tagged_Ethernet_unpack(self):
+        """Test pack method of Ethernet class including VLAN tag."""
+        raw = b'\x00\x15\xaf\xd58\x98\x00\x1f:>'
+        raw += b'\x9a\xcf\x81\x00!^\x08\x00testdata'
+        expected = Ethernet(destination='00:15:af:d5:38:98',
+                            source='00:1f:3a:3e:9a:cf', vlan=VLAN(pcp=1,
+                                                                  vid=350),
+                            ether_type=0x800, data=b'testdata')
+        expected.pack()
+        unpacked = Ethernet()
+        unpacked.unpack(raw)
+        self.assertEqual(unpacked, expected)
+
+
+class TestVLAN(unittest.TestCase):
+    """Test VLAN headers."""
+
+    def test_VLAN_pack(self):
+        """Test pack method of VLAN class."""
+        vlan = VLAN(pcp=3, vid=20)
+        packed = vlan.pack()
+        expected = b'\x81\x00`\x14'
+        self.assertEqual(packed, expected)
+
+    def test_VLAN_unpack(self):
+        """Test unpack method of VLAN class."""
+        raw = b'\x81\x00\xa0{'
+        expected = VLAN(pcp=5, vid=123)
+        unpacked = VLAN()
+        unpacked.unpack(raw)
+        self.assertEqual(unpacked, expected)
+
+    def test_unpack_wrong_tpid(self):
+        """Raise UnpackException if the tpid is not VLAN_TPID."""
+        raw = b'\x12\x34\xa0{'
+        vlan = VLAN()
+        with self.assertRaises(UnpackException):
+            vlan.unpack(raw)
+
+
 class TestIPv4(unittest.TestCase):
     """Test IPv4 packets."""