Adtran ONU: Updated support for MIB Sync and refactored OMCI service delivery task

Change-Id: I915a46db0d7eca3d8b1d9fa5cf57efbed8f2b6ef
diff --git a/voltha/adapters/adtran_olt/flow/flow_entry.py b/voltha/adapters/adtran_olt/flow/flow_entry.py
index 6d5ea37..fa4fbed 100644
--- a/voltha/adapters/adtran_olt/flow/flow_entry.py
+++ b/voltha/adapters/adtran_olt/flow/flow_entry.py
@@ -19,7 +19,7 @@
 from utility_evc import UtilityEVC
 import voltha.core.flow_decomposer as fd
 from voltha.core.flow_decomposer import *
-from voltha.protos.openflow_13_pb2 import OFPP_MAX
+from voltha.protos.openflow_13_pb2 import OFPP_MAX, OFPP_CONTROLLER
 from twisted.internet.defer import returnValue, inlineCallbacks, gatherResults
 
 log = structlog.get_logger()
@@ -55,19 +55,30 @@
 
     Note: Since only E-LINE is supported, modification of an existing EVC is not performed.
     """
+    class PortType(IntEnum):
+        NNI = 0         # NNI Port
+        UNI = 1         # UNI Port
+        PON = 2         # PON Port (all UNIs on PON)
+        CONTROLLER = 3  # Controller port (packet in/out)
+
     class FlowDirection(IntEnum):
         UPSTREAM = 0          # UNI port to NNI Port
         DOWNSTREAM = 1        # NNI port to UNI Port
-        NNI = 2               # NNI port to NNI Port
-        UNI = 3               # UNI port to UNI Port
-        OTHER = 4             # Unable to determine
+        CONTROLLER_UNI = 2    # Trap packet on UNI and send to controller
+        NNI_PON = 3           # NNI port to PON Port (all UNIs) - perhaps multicast?
 
-    _flow_dir_map = {
-        (FlowDirection.UNI, FlowDirection.NNI): FlowDirection.UPSTREAM,
-        (FlowDirection.NNI, FlowDirection.UNI): FlowDirection.DOWNSTREAM,
-        (FlowDirection.UNI, FlowDirection.UNI): FlowDirection.UNI,
-        (FlowDirection.NNI, FlowDirection.NNI): FlowDirection.NNI,
-    }
+        # The following are not yet supported
+        CONTROLLER_NNI = 4    # Trap packet on NNI and send to controller
+        CONTROLLER_PON = 5    # Trap packet on all UNIs of a PON and send to controller
+        NNI_NNI = 6           # NNI port to NNI Port
+        UNI_UNI = 7           # UNI port to UNI Port
+        OTHER = 9             # Unable to determine
+        NNI = 10              # Deprecate in v2.0
+        UNI = 11              # Deprecate in v2.0
+        
+    upstream_flow_types = {FlowDirection.UPSTREAM, FlowDirection.CONTROLLER_UNI}
+    downstream_flow_types = {FlowDirection.DOWNSTREAM, FlowDirection.NNI_PON}
+
     LEGACY_CONTROL_VLAN = 4000
 
     # Well known EtherTypes
@@ -76,6 +87,7 @@
         IPv4 = 0x0800
         IPv6 = 0x86DD
         ARP = 0x0806
+        LLDP = 0x88CC
 
     # Well known IP Protocols
     class IpProtocol(IntEnum):
diff --git a/voltha/adapters/adtran_onu/adtran_onu.py b/voltha/adapters/adtran_onu/adtran_onu.py
index 58f635e..53addc2 100755
--- a/voltha/adapters/adtran_onu/adtran_onu.py
+++ b/voltha/adapters/adtran_onu/adtran_onu.py
@@ -27,6 +27,8 @@
 from omci.adtn_capabilities_task import AdtnCapabilitiesTask
 from omci.adtn_get_mds_task import AdtnGetMdsTask
 from omci.adtn_mib_sync import AdtnMibSynchronizer
+from omci.adtn_mib_resync_task import AdtnMibResyncTask
+from omci.adtn_mib_reconcile_task import AdtnMibReconcileTask
 from copy import deepcopy
 
 _ = third_party
@@ -40,7 +42,7 @@
                                                device_handler_class=AdtranOnuHandler,
                                                name='adtran_onu',
                                                vendor='Adtran Inc.',
-                                               version='1.18',
+                                               version='1.19',
                                                device_type='adtran_onu',
                                                vendor_id='ADTN',
                                                accepts_add_remove_flow_updates=False),  # TODO: Support flow-mods
@@ -50,8 +52,9 @@
         self.adtran_omci['mib-synchronizer']['state-machine'] = AdtnMibSynchronizer
         self.adtran_omci['mib-synchronizer']['tasks']['get-mds'] = AdtnGetMdsTask
         self.adtran_omci['mib-synchronizer']['tasks']['mib-audit'] = AdtnGetMdsTask
+        self.adtran_omci['mib-synchronizer']['tasks']['mib-resync'] = AdtnMibResyncTask
+        self.adtran_omci['mib-synchronizer']['tasks']['mib-reconcile'] = AdtnMibReconcileTask
         self.adtran_omci['omci-capabilities']['tasks']['get-capabilities'] = AdtnCapabilitiesTask
-
         # TODO: Continue to customize adtran_omci here as needed
 
         self._omci_agent = OpenOMCIAgent(self.adapter_agent.core,
@@ -133,7 +136,7 @@
 
     def receive_proxied_message(self, proxy_address, msg):
         self.log.debug('receive-proxied-message', proxy_address=proxy_address,
-                  device_id=proxy_address.device_id, msg=binascii.hexlify(msg))
+                       device_id=proxy_address.device_id, msg=binascii.hexlify(msg))
         # Device_id from the proxy_address is the olt device id. We need to
         # get the onu device id using the port number in the proxy_address
         device = self.adapter_agent.get_child_device_with_proxy_address(proxy_address)
diff --git a/voltha/adapters/adtran_onu/adtran_onu_handler.py b/voltha/adapters/adtran_onu/adtran_onu_handler.py
index 86b9991..4d3b5db 100644
--- a/voltha/adapters/adtran_onu/adtran_onu_handler.py
+++ b/voltha/adapters/adtran_onu/adtran_onu_handler.py
@@ -82,6 +82,7 @@
         # TODO: Some of these could be dynamically chosen
         self.vlan_tcis_1 = 0x900
         self.mac_bridge_service_profile_entity_id = self.vlan_tcis_1
+        self.gal_enet_profile_entity_id = 0     # Was 0x100, but ONU seems to overwrite and use zero
 
         # Assume no XPON support unless we get an vont-ani/ont-ani/venet create
         self.xpon_support = False    # xPON no longer available
@@ -354,11 +355,11 @@
             if flow_entry.flow_id in self._flows:
                 valid_flows.add(flow_entry.flow_id)
 
-            if flow_entry is None or flow_entry.flow_direction not in {FlowEntry.FlowDirection.UPSTREAM,
-                                                                       FlowEntry.FlowDirection.DOWNSTREAM}:
+            if flow_entry is None or flow_entry.flow_direction not in {FlowEntry.upstream_flow_types,
+                                                                       FlowEntry.downstream_flow_types}:
                 continue
 
-            is_upstream = flow_entry.flow_direction == FlowEntry.FlowDirection.UPSTREAM
+            is_upstream = flow_entry.flow_direction in FlowEntry.upstream_flow_types
 
             # Ignore untagged upstream etherType flows. These are trapped at the
             # OLT and the default flows during initial OMCI service download will
@@ -772,7 +773,6 @@
 
         # If the PON has already synchronized, add the logical port now
         # since we know we have been activated
-
         if self._pon is not None and self.openomci.connected:
             uni_port.add_logical_port(ofp_port_no, subscriber_vlan=subscriber_vlan)
 
diff --git a/voltha/adapters/adtran_onu/omci/adtn_capabilities_task.py b/voltha/adapters/adtran_onu/omci/adtn_capabilities_task.py
index 6be8fd7..6dbed03 100644
--- a/voltha/adapters/adtran_onu/omci/adtn_capabilities_task.py
+++ b/voltha/adapters/adtran_onu/omci/adtn_capabilities_task.py
@@ -35,14 +35,18 @@
                 'supported-message-types': {set of supported message types}
               }
     """
+    name = "Adtran ONU Capabilities Task"
+
     def __init__(self, omci_agent, device_id):
         """
         Class initialization
 
-        :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+        :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
         :param device_id: (str) ONU Device ID
         """
         super(AdtnCapabilitiesTask, self).__init__(omci_agent, device_id)
+
+        self.name = AdtnCapabilitiesTask.name
         self._omci_managed = False      # TODO: Look up capabilities/model number
 
     @property
diff --git a/voltha/adapters/adtran_onu/omci/adtn_install_flow.py b/voltha/adapters/adtran_onu/omci/adtn_install_flow.py
index b5fcb40..5f155f1 100644
--- a/voltha/adapters/adtran_onu/omci/adtn_install_flow.py
+++ b/voltha/adapters/adtran_onu/omci/adtn_install_flow.py
@@ -44,7 +44,7 @@
         """
         Class initialization
 
-        :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+        :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
         :param handler: (AdtranOnuHandler) ONU Handler
         :param flow_entry: (FlowEntry) Flow to install
         """
@@ -59,7 +59,7 @@
         self._flow_entry = flow_entry
 
         # TODO: Cleanup below that is not needed
-        is_upstream = flow_entry.flow_direction == FlowEntry.FlowDirection.UPSTREAM
+        is_upstream = flow_entry.flow_direction in FlowEntry.upstream_flow_types
         uni_port = flow_entry.in_port if is_upstream else flow_entry.out_port
         pon_port = flow_entry.out_port if is_upstream else flow_entry.in_port
 
diff --git a/voltha/adapters/adtran_onu/omci/adtn_mib_download_task.py b/voltha/adapters/adtran_onu/omci/adtn_mib_download_task.py
index 136e2e2..5b852a1 100644
--- a/voltha/adapters/adtran_onu/omci/adtn_mib_download_task.py
+++ b/voltha/adapters/adtran_onu/omci/adtn_mib_download_task.py
@@ -61,8 +61,8 @@
         """
         Class initialization
 
-        :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
-        :param device_id: (str) ONU Device ID
+        :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
+        :param handler: (OnuHandler) ONU Device Handler
         """
         super(AdtnMibDownloadTask, self).__init__(AdtnMibDownloadTask.name,
                                                   omci_agent,
@@ -90,7 +90,7 @@
         #
         self._ieee_mapper_service_profile_entity_id = self._pon.hsi_8021p_mapper_entity_id
         self._mac_bridge_port_ani_entity_id = self._pon.hsi_mac_bridge_port_ani_entity_id
-        self._gal_enet_profile_entity_id = 0x100
+        self._gal_enet_profile_entity_id = self._handler.gal_enet_profile_entity_id
 
         # Next to are specific     TODO: UNI lookups here or uni specific install !!!
         self._ethernet_uni_entity_id = self._handler.uni_ports[0].entity_id
diff --git a/voltha/adapters/adtran_onu/omci/adtn_mib_reconcile_task.py b/voltha/adapters/adtran_onu/omci/adtn_mib_reconcile_task.py
new file mode 100644
index 0000000..872cedb
--- /dev/null
+++ b/voltha/adapters/adtran_onu/omci/adtn_mib_reconcile_task.py
@@ -0,0 +1,184 @@
+#
+# Copyright 2018 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.
+#
+from twisted.internet.defer import returnValue
+from voltha.extensions.omci.omci_defs import *
+from voltha.extensions.omci.omci_entities import Ieee8021pMapperServiceProfile
+from voltha.extensions.omci.tasks.mib_reconcile_task import MibReconcileTask
+from voltha.extensions.omci.database.mib_db_api import ATTRIBUTES_KEY
+from twisted.internet.defer import  inlineCallbacks
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_me import MEFrame
+
+OP = EntityOperations
+RC = ReasonCodes
+AA = AttributeAccess
+
+
+class AdtnMibReconcileTask(MibReconcileTask):
+    """
+    Adtran ONU OpenOMCI MIB Reconcile Task
+
+    For some crazy reason, the ADTRAN ONU does not report the IEEE802.1p Mapper ME
+    in the ONU upload even though it does exists.  This results in an 'instance
+    exists' error when trying to create it on the ONU
+    """
+    name = "Adtran MIB Reconcile Task"
+
+    def __init__(self, omci_agent, device_id, diffs):
+        super(AdtnMibReconcileTask, self).__init__(omci_agent, device_id, diffs)
+
+        self.name = AdtnMibReconcileTask.name
+        self._me_130_okay = False   # Set true once bug is fixed (auto detect)
+        self._omci_managed = False  # Set once ONU Data tracking of MIB-Data-Sync supported
+
+    @inlineCallbacks
+    def fix_olt_only(self, olt, onu_db, olt_db):
+        """
+        Fix ME's that were only found on the OLT. For OLT only MEs there are
+        the following things that will be checked.
+
+            o ME's that do not have an OpenOMCI class decoder. These are stored
+              as binary blobs in the MIB database. Since the OLT will never
+              create these (all are learned from ONU), it is assumed the ONU
+              has removed them for some purpose. So delete them from the OLT
+              database.
+
+            o For ME's that are created by the ONU (no create/delete access), the
+              MEs 'may' not be on the ONU because of a reboot or an OLT created
+              ME was deleted and the ONU gratuitously removes it.  So delete them
+              from the OLT database.
+
+            o For ME's that are created by the OLT/OpenOMCI, delete them from the
+              ONU
+
+        :param olt: (list(int,int)) List of tuples where (class_id, inst_id)
+        :param onu_db: (dict) ONU Database snapshot at time of audit
+        :param olt_db: (dict) OLT Database snapshot at time of audit
+
+        :return: (int, int) successes, failures
+        """
+        # Has IEEE 802.1p reporting Bug fixed?
+
+        if self._me_130_okay or Ieee8021pMapperServiceProfile.class_id in onu_db:
+            self._me_130_okay = True
+            returnValue(super(AdtnMibReconcileTask, self).fix_olt_only(olt, onu_db, olt_db))
+
+        ############################
+        # Base class handles all but ME 130
+        local_mes = {Ieee8021pMapperServiceProfile.class_id}
+        not_manual = [(cid, eid) for cid, eid in olt if cid not in local_mes]
+
+        results = yield super(AdtnMibReconcileTask, self).fix_olt_only(not_manual,
+                                                                       onu_db,
+                                                                       olt_db)
+        successes = results[0]
+        failures = results[1]
+
+        # If IEEE 802.1p mapper needs to be checked, do it manually as the IBONT 602
+        # manipulates it during MEF EVC/EVC-Map creation
+        for cid in local_mes:
+            class_entry = olt_db.get(cid, None)
+
+            if class_entry is not None:
+                entries = {k: v for k, v in class_entry.items() if isinstance(k, int)}
+                for eid, instance in entries.items():
+                    try:
+                        self.strobe_watchdog()
+                        results = yield self.manual_verification(cid, eid, instance[ATTRIBUTES_KEY])
+                        successes += results[0]
+                        failures += results[1]
+
+                    except Exception as _e:
+                        failures += 1
+
+        returnValue((successes, failures))
+
+    @inlineCallbacks
+    def update_mib_data_sync(self):
+        """ IBONT version does not support MDS"""
+        if self._omci_managed:
+            results = yield super(AdtnMibReconcileTask, self).update_mib_data_sync()
+            returnValue(results)
+
+        returnValue((1, 0))
+
+    @inlineCallbacks
+    def manual_verification(self, cid, eid, attributes):
+        # Trim off read-only attributes from ones passed in
+
+        me_map = self._device.me_map
+        ro_set = {AA.R}
+        ro_attrs = {attr.field.name for attr in me_map[cid].attributes
+                    if attr.access == ro_set}
+        attributes = {k: v for k, v in attributes.items() if k not in ro_attrs}
+        attributes_to_fix = dict()
+
+        try:
+            while len(attributes):
+                frame = MEFrame(me_map[cid], eid, attributes).get()
+                self.strobe_watchdog()
+                results = yield self._device.omci_cc.send(frame)
+                omci_message = results.fields['omci_message'].fields
+                status = omci_message['success_code']
+
+                if status == RC.UnknownEntity.value:
+                    self.strobe_watchdog()
+                    results = yield self.create_instance(me_map[cid], eid, attributes)
+                    returnValue((results[0], results[1]))
+
+                if status != RC.Success.value:
+                    self.log.error('manual-check-get-failed', cid=cid, eid=eid,
+                                   attributes=attributes, status=status)
+                    returnValue((1, 0))
+
+                onu_attr = {k: v for k, v in omci_message['data'].items()}
+                attributes_to_fix.update({k: v for k, v in onu_attr.items()
+                                         if k in attributes and v != attributes[k]})
+                attributes = {k: v for k, v in attributes if k not in onu_attr.keys()}
+
+            if len(attributes_to_fix) > 0:
+                try:
+                    frame = MEFrame(me_map[cid], eid, attributes_to_fix).set()
+                    self.strobe_watchdog()
+                    yield self._device.omci_cc.send(frame)
+                    returnValue((1, 0))
+
+                except Exception as _e:
+                    returnValue((0, 1))
+
+        except Exception as e:
+            self.log.exception('manual-check-failed', e=e, cid=cid, eid=eid)
+            raise
+
+    @inlineCallbacks
+    def create_instance(self, cid, eid, attributes):
+        try:
+            me_map = self._device.me_map
+            frame = MEFrame(me_map[cid], eid, attributes).create()
+
+            self.strobe_watchdog()
+            results = yield self._device.omci_cc.send(frame)
+            status = results.fields['omci_message'].fields['success_code']
+            if status == RC.Success.value or status == RC.InstanceExists.value:
+                returnValue((1, 0))
+
+            self.log.error('manual-check-create-failed', cid=cid, eid=eid,
+                           attributes=attributes, status=status)
+            returnValue((0, 1))
+
+        except Exception as e:
+            self.log.exception('manual-check-failed', e=e, cid=cid, eid=eid)
+            raise
diff --git a/voltha/adapters/adtran_onu/omci/adtn_mib_resync_task.py b/voltha/adapters/adtran_onu/omci/adtn_mib_resync_task.py
new file mode 100644
index 0000000..500ee02
--- /dev/null
+++ b/voltha/adapters/adtran_onu/omci/adtn_mib_resync_task.py
@@ -0,0 +1,67 @@
+#
+# 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.
+#
+from voltha.extensions.omci.tasks.mib_resync_task import MibResyncTask
+from voltha.extensions.omci.omci_entities import GalEthernetProfile, GemPortNetworkCtp, \
+    Ieee8021pMapperServiceProfile
+
+
+class AdtnMibResyncTask(MibResyncTask):
+    """
+    ADTRAN MIB resynchronization Task
+
+    The ADTRAN IBONT 602 does not report the current value of the GAL Ethernet
+    Payload size, it is always 0.
+
+    Also, the MEF EVC/EVC-MAP code monitors GEM Port CTP ME
+    """
+    def __init__(self, omci_agent, device_id):
+        """
+        Class initialization
+
+        :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
+        :param device_id: (str) ONU Device ID
+        """
+        super(AdtnMibResyncTask, self).__init__(omci_agent, device_id)
+        self.omci_fixed = False
+
+    def compare_mibs(self, db_copy, db_active):
+        """
+        Compare the our db_copy with the ONU's active copy
+
+        :param db_copy: (dict) OpenOMCI's copy of the database
+        :param db_active: (dict) ONU's database snapshot
+        :return: (dict), (dict), (list)  Differences
+        """
+        on_olt_only, on_onu_only, attr_diffs = super(AdtnMibResyncTask, self).\
+            compare_mibs(db_copy, db_active)
+
+        if not self.omci_fixed:
+            # Exclude 'max_gem_payload_size' in GAL Ethernet Profile
+            attr_diffs = [attr for attr in attr_diffs
+                          if attr[0] != GalEthernetProfile.class_id
+                          or attr[2] != 'max_gem_payload_size']
+
+            # Exclude any changes to GEM Port Network CTP
+            attr_diffs = [attr for attr in attr_diffs
+                          if attr[0] != GemPortNetworkCtp.class_id]
+
+            if on_olt_only is not None:
+                # Exclude IEEE 8021.p Mapper Service Profile from OLT Only as not
+                # reported in current IBONT 602 software
+                on_olt_only = [(cid, eid) for cid, eid in on_olt_only
+                               if cid != Ieee8021pMapperServiceProfile.class_id]
+
+        return on_olt_only, on_onu_only, attr_diffs
diff --git a/voltha/adapters/adtran_onu/omci/adtn_mib_sync.py b/voltha/adapters/adtran_onu/omci/adtn_mib_sync.py
index 50494e6..b2d3db4 100644
--- a/voltha/adapters/adtran_onu/omci/adtn_mib_sync.py
+++ b/voltha/adapters/adtran_onu/omci/adtn_mib_sync.py
@@ -37,8 +37,31 @@
                                                   advertise_events=advertise_events,
                                                   audit_delay=AdtnMibSynchronizer.ADTN_AUDIT_DELAY,
                                                   resync_delay=AdtnMibSynchronizer.ADTN_RESYNC_DELAY)
+        self._first_in_sync = True
         self._omci_managed = False      # TODO: Look up model number/check handler
 
+    def increment_mib_data_sync(self):
+        if self._omci_managed:
+            super(AdtnMibSynchronizer, self).increment_mib_data_sync()
+
+        # IBONT 602 does not support MDS
+        self._mib_data_sync = 0
+
+    def on_enter_in_sync(self):
+        """ Early first sync """
+        if not self._omci_managed:
+            # IBONT 602 does not support MDS, accelerate first forced resync
+            # after a MIB reset occurs or on first startup
+            if self._first_in_sync:
+                self._first_in_sync = False
+                # self._audit_delay = 10            # Re-enable after BBWF
+                # self._resync_delay = 10
+            else:
+                self._audit_delay = MibSynchronizer.DEFAULT_AUDIT_DELAY
+                self._resync_delay = AdtnMibSynchronizer.ADTN_RESYNC_DELAY
+
+        super(AdtnMibSynchronizer, self).on_enter_in_sync()
+
     def on_enter_auditing(self):
         """
         Perform a MIB Audit.  If our last MIB resync was too long in the
@@ -56,3 +79,7 @@
 
     def _check_if_mib_data_sync_supported(self):
         return False    # TODO: Look up to see if we are/check handler
+
+    def on_mib_reset_response(self, topic, msg):
+        self._first_in_sync = True
+        super(AdtnMibSynchronizer, self).on_mib_reset_response(topic, msg)
diff --git a/voltha/adapters/adtran_onu/omci/adtn_remove_flow.py b/voltha/adapters/adtran_onu/omci/adtn_remove_flow.py
index 9ca1c19..214a5aa 100644
--- a/voltha/adapters/adtran_onu/omci/adtn_remove_flow.py
+++ b/voltha/adapters/adtran_onu/omci/adtn_remove_flow.py
@@ -47,7 +47,7 @@
         """
         Class initialization
 
-        :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+        :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
         :param handler: (AdtranOnuHandler) ONU Handler
         :param flow_entry: (FlowEntry) Flow to install
         """
@@ -66,7 +66,7 @@
         # self._input_tpid = AdtnRemoveFlowTask.default_tpid
         # self._output_tpid = AdtnRemoveFlowTask.default_tpid
 
-        is_upstream = flow_entry.flow_direction == FlowEntry.FlowDirection.UPSTREAM
+        is_upstream = flow_entry.flow_direction in FlowEntry.upstream_flow_types
         # uni_port = flow_entry.in_port if is_upstream else flow_entry.out_port
         pon_port = flow_entry.out_port if is_upstream else flow_entry.in_port
 
diff --git a/voltha/adapters/adtran_onu/omci/adtn_service_download_task.py b/voltha/adapters/adtran_onu/omci/adtn_service_download_task.py
index 6dc510a..e4839e5 100644
--- a/voltha/adapters/adtran_onu/omci/adtn_service_download_task.py
+++ b/voltha/adapters/adtran_onu/omci/adtn_service_download_task.py
@@ -55,12 +55,13 @@
     task_priority = Task.DEFAULT_PRIORITY + 10
     default_tpid = 0x8100                       # TODO: Move to a better location
     name = "ADTRAN Service Download Task"
+    free_tcont_alloc_id = 0xFFFF
 
     def __init__(self, omci_agent, handler):
         """
         Class initialization
 
-        :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+        :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
         :param device_id: (str) ONU Device ID
         """
         super(AdtnServiceDownloadTask, self).__init__(AdtnServiceDownloadTask.name,
@@ -72,8 +73,8 @@
         self._onu_device = omci_agent.get_device(handler.device_id)
         self._local_deferred = None
         self._pon = handler.pon_port()
+        self._extended_vlan_me_created = False
 
-        # self._vlan_tcis_1 = self._handler.vlan_tcis_1
         self._input_tpid = AdtnServiceDownloadTask.default_tpid
         self._output_tpid = AdtnServiceDownloadTask.default_tpid
 
@@ -91,7 +92,7 @@
         # TODO: Probably need to store many of these in the appropriate object (UNI, PON,...)
         #
         self._ieee_mapper_service_profile_entity_id = self._pon.hsi_8021p_mapper_entity_id
-        self._gal_enet_profile_entity_id = 0x100
+        self._gal_enet_profile_entity_id = self._handler.gal_enet_profile_entity_id
 
         # Next to are specific
         self._ethernet_uni_entity_id = self._handler.uni_ports[0].entity_id
@@ -196,6 +197,9 @@
 
             except TimeoutError as e:
                 self.deferred.errback(failure.Failure(e))
+
+            except Exception as e:
+                self.deferred.errback(failure.Failure(e))
         else:
             # TODO: Provide better error reason, what was missing...
             e = ServiceResourcesFailure('Required resources are not available')
@@ -216,17 +220,25 @@
             #            - ONU created TCONT (created on ONU startup)
 
             tcont_idents = self._onu_device.query_mib(Tcont.class_id)
-            self.log.debug('tcont-idents', tcont_idents=tcont_idents)
+            self.log.info('tcont-idents', tcont_idents=tcont_idents)
 
             for tcont in self._pon.tconts.itervalues():
-                free_entity_id = next((k for k, v in tcont_idents.items()
-                                       if isinstance(k, int) and
-                                       v.get('attributes', {}).get('alloc_id', 0) == 0xFFFF), None)
-                if free_entity_id is None:
-                    self.log.error('no-available-tconts')
-                    break
-                # TODO: Need to restore on failure.  Need to check status/results
-                yield tcont.add_to_hardware(omci_cc, free_entity_id)
+                if tcont.entity_id is None:
+                    free_entity_id = next((k for k, v in tcont_idents.items()
+                                           if isinstance(k, int) and
+                                           v.get('attributes', {}).get('alloc_id', 0) ==
+                                           AdtnServiceDownloadTask.free_tcont_alloc_id), None)
+
+                    if free_entity_id is None:
+                        self.log.error('no-available-tconts')
+                        raise ServiceResourcesFailure('No Available TConts')
+
+                    try:
+                        yield tcont.add_to_hardware(omci_cc, free_entity_id)
+
+                    except Exception as e:
+                        self.log.exception('tcont-set', e=e, eid=free_entity_id)
+                        raise
 
             ################################################################################
             # GEMS  (GemPortNetworkCtp and GemInterworkingTp)
@@ -254,17 +266,20 @@
             #              - Ieee8021pMapperServiceProfile
             #              - GalEthernetProfile
             #
-
             for gem_port in self._pon.gem_ports.itervalues():
-                tcont = gem_port.tcont
-                if tcont is None:
-                    self.log.error('unknown-tcont-reference', gem_id=gem_port.gem_id)
-                    continue
-                # TODO: Need to restore on failure.  Need to check status/results
-                yield gem_port.add_to_hardware(omci_cc,
-                                               tcont.entity_id,
-                                               self._ieee_mapper_service_profile_entity_id,
-                                               self._gal_enet_profile_entity_id)
+                if not gem_port.in_hardware:
+                    tcont = gem_port.tcont
+                    if tcont is None:
+                        raise Exception('unknown-tcont-reference', gem_id=gem_port.gem_id)
+
+                    try:
+                        yield gem_port.add_to_hardware(omci_cc,
+                                                       tcont.entity_id,
+                                                       self._ieee_mapper_service_profile_entity_id,
+                                                       self._gal_enet_profile_entity_id)
+                    except Exception as e:
+                        self.log.exception('gem-add-failed', e=e, gem=gem_port)
+                        raise
 
             ################################################################################
             # Update the IEEE 802.1p Mapper Service Profile config
@@ -276,8 +291,11 @@
             #
             # TODO: All p-bits currently go to the one and only GEMPORT ID for now
             gem_ports = self._pon.gem_ports
-            gem_entity_ids = [gem_port.entity_id for _, gem_port in gem_ports.items()] \
-                if len(gem_ports) else [OmciNullPointer]
+
+            if len(gem_ports):
+                gem_entity_ids = [gem_port.entity_id for _, gem_port in gem_ports.items()]
+            else:
+                gem_entity_ids = [OmciNullPointer]
 
             frame = Ieee8021pMapperServiceProfileFrame(
                 self._ieee_mapper_service_profile_entity_id,  # 802.1p mapper Service Mapper Profile ID
@@ -307,6 +325,7 @@
             ).create()
             results = yield omci_cc.send(frame)
             self.check_status_and_state(results, 'create-extended-vlan-tagging-operation-configuration-data')
+            self._extended_vlan_me_created = True
 
             ################################################################################
             # Update Extended VLAN Tagging Operation Config Data
@@ -367,10 +386,12 @@
 
         except TimeoutError as e:
             self.log.warn('rx-timeout-download', frame=hexlify(frame))
+            self.cleanup_on_error()
             raise
 
         except Exception as e:
             self.log.exception('omci-setup-2', e=e)
+            self.cleanup_on_error()
             raise
 
         returnValue(None)
@@ -411,11 +432,39 @@
 
         returnValue(None)
 
+    @inlineCallbacks
+    def cleanup_on_error(self):
 
+        omci_cc = self._onu_device.omci_cc
 
+        if self._extended_vlan_me_created:
+            try:
+                eid = self._mac_bridge_service_profile_entity_id
+                frame = ExtendedVlanTaggingOperationConfigurationDataFrame(eid).delete()
+                results = yield omci_cc.send(frame)
+                status = results.fields['omci_message'].fields['success_code']
+                self.log.debug('delete-extended-vlan-me', status=status)
 
+            except Exception as e:
+                self.log.exception('extended-vlan-cleanup', e=e)
+                # Continue processing
 
+        for gem_port in self._pon.gem_ports.itervalues():
+            if gem_port.in_hardware:
+                try:
+                    yield gem_port.remove_from_hardware(omci_cc)
 
+                except Exception as e:
+                    self.log.exception('gem-port-cleanup', e=e)
+                    # Continue processing
 
+        for tcont in self._pon.tconts.itervalues():
+            if tcont.entity_id != AdtnServiceDownloadTask.free_tcont_alloc_id:
+                try:
+                    yield tcont.remove_from_hardware(omci_cc)
 
+                except Exception as e:
+                    self.log.exception('tcont-cleanup', e=e)
+                    # Continue processing
 
+        returnValue('Cleanup Complete')
diff --git a/voltha/adapters/adtran_onu/omci/omci.py b/voltha/adapters/adtran_onu/omci/omci.py
index 15244b8..5966d2c 100644
--- a/voltha/adapters/adtran_onu/omci/omci.py
+++ b/voltha/adapters/adtran_onu/omci/omci.py
@@ -262,21 +262,18 @@
         from voltha.extensions.omci.omci_cc import OMCI_CC, OmciCCRxEvents
 
         # OMCI MIB Database sync status
-
         bus = self._onu_omci_device.event_bus
         topic = OnuDeviceEntry.event_bus_topic(self._handler.device_id,
                                                OnuDeviceEvents.MibDatabaseSyncEvent)
         self._in_sync_subscription = bus.subscribe(topic, self.in_sync_handler)
 
         # OMCI Capabilities (MEs and Message Types
-
         bus = self._onu_omci_device.event_bus
         topic = OnuDeviceEntry.event_bus_topic(self._handler.device_id,
                                                OnuDeviceEvents.OmciCapabilitiesEvent)
         self._capabilities_subscription = bus.subscribe(topic, self.capabilities_handler)
 
         # OMCI-CC Connectivity Events (for reachability/heartbeat)
-
         bus = self._onu_omci_device.omci_cc.event_bus
         topic = OMCI_CC.event_bus_topic(self._handler.device_id,
                                         OmciCCRxEvents.Connectivity)
@@ -329,6 +326,7 @@
         if self._capabilities_subscription is not None:
             from adtn_mib_download_task import AdtnMibDownloadTask
             from adtn_service_download_task import AdtnServiceDownloadTask
+            self._mib_download_task = None
 
             def success(_results):
                 if self._mib_downloaded:
@@ -349,12 +347,13 @@
             if not self._mib_downloaded:
                 self._mib_download_task = AdtnMibDownloadTask(self.omci_agent,
                                                               self._handler)
-            else:
+            elif not self._service_downloaded:
                 self._mib_download_task = AdtnServiceDownloadTask(self.omci_agent,
                                                                   self._handler)
-            self._mib_download_deferred = \
-                self._onu_omci_device.task_runner.queue_task(self._mib_download_task)
-            self._mib_download_deferred.addCallbacks(success, failure)
+            if self._mib_download_task is not None:
+                self._mib_download_deferred = \
+                    self._onu_omci_device.task_runner.queue_task(self._mib_download_task)
+                self._mib_download_deferred.addCallbacks(success, failure)
 
     def onu_is_reachable(self, _topic, msg):
         """
diff --git a/voltha/adapters/adtran_onu/onu_gem_port.py b/voltha/adapters/adtran_onu/onu_gem_port.py
index 0e02ead..a8fe3ed 100644
--- a/voltha/adapters/adtran_onu/onu_gem_port.py
+++ b/voltha/adapters/adtran_onu/onu_gem_port.py
@@ -17,6 +17,7 @@
 from voltha.adapters.adtran_olt.xpon.gem_port import GemPort
 from twisted.internet.defer import inlineCallbacks, returnValue
 from voltha.extensions.omci.omci_me import GemPortNetworkCtpFrame, GemInterworkingTpFrame
+from voltha.extensions.omci.omci_defs import ReasonCodes
 
 
 class OnuGemPort(GemPort):
@@ -44,6 +45,8 @@
                                          name=name,
                                          handler=handler)
         self._entity_id = entity_id
+        self._tcont_entity_id = None
+        self._interworking = False
         self.log = structlog.get_logger(device_id=handler.device_id, gem_id=gem_id)
 
     @property
@@ -54,6 +57,10 @@
     def encryption(self):
         return self._encryption
 
+    @property
+    def in_hardware(self):
+        return self._tcont_entity_id is not None and self._interworking
+
     @encryption.setter
     def encryption(self, value):
         assert isinstance(value, bool), 'encryption is a boolean'
@@ -78,79 +85,110 @@
                         tcont_entity_id,
                         ieee_mapper_service_profile_entity_id,
                         gal_enet_profile_entity_id):
+
         self.log.debug('add-to-hardware', gem_id=self.gem_id,
+                       gem_entity_id=self.entity_id,
                        tcont_entity_id=tcont_entity_id,
                        ieee_mapper_service_profile_entity_id=ieee_mapper_service_profile_entity_id,
                        gal_enet_profile_entity_id=gal_enet_profile_entity_id)
-        try:
-            direction = "downstream" if self.multicast else "bi-directional"
-            assert not self.multicast, 'MCAST is not supported yet'
 
-            frame = GemPortNetworkCtpFrame(
+        if self._tcont_entity_id is not None and self._tcont_entity_id != tcont_entity_id:
+            raise KeyError('GEM Port already assigned to TCONT: {}'.format(self._tcont_entity_id))
+
+        results = None
+        if self._tcont_entity_id is None:
+            try:
+                direction = "downstream" if self.multicast else "bi-directional"
+                assert not self.multicast, 'MCAST is not supported yet'
+
+                frame = GemPortNetworkCtpFrame(
+                        self.entity_id,          # same entity id as GEM port
+                        port_id=self.gem_id,
+                        tcont_id=tcont_entity_id,
+                        direction=direction,
+                        upstream_tm=0x8000      # TM ID, 32768 unique ID set in TD set  TODO: Parameterize
+                                                # This is Priority Queue ME with this entity ID
+                                                # and the ME's related port value is 0x01.00.0007
+                                                # which is  slot=0x01, tcont# = 0x00, priority= 0x0007
+                ).create()
+                results = yield omci.send(frame)
+
+                status = results.fields['omci_message'].fields['success_code']
+                error_mask = results.fields['omci_message'].fields['parameter_error_attributes_mask']
+                self.log.debug('create-gem-port-network-ctp', status=status, error_mask=error_mask)
+
+                if status == ReasonCodes.Success or status == ReasonCodes.InstanceExists:
+                    self._tcont_entity_id = tcont_entity_id
+                else:
+                    raise Exception('GEM Port create failed with status: {}'.format(status))
+
+            except Exception as e:
+                self.log.exception('gemport-create', e=e)
+                raise
+
+        if not self._interworking:
+            try:
+                extra = {'gal_loopback_configuration': 0}   # No loopback
+
+                frame = GemInterworkingTpFrame(
                     self.entity_id,          # same entity id as GEM port
-                    port_id=self.gem_id,
-                    tcont_id=tcont_entity_id,
-                    direction=direction,
-                    upstream_tm=0x8000      # TM ID, 32768 unique ID set in TD set  TODO: Parameterize
-                                            # This is Priority Queue ME with this entity ID
-                                            # and the ME's related port value is 0x01.00.0007
-                                            # which is  slot=0x01, tcont# = 0x00, priority= 0x0007
-            ).create()
-            results = yield omci.send(frame)
+                    gem_port_network_ctp_pointer=self.entity_id,
+                    interworking_option=5,                             # IEEE 802.1
+                    service_profile_pointer=ieee_mapper_service_profile_entity_id,
+                    interworking_tp_pointer=0x0,
+                    pptp_counter=1,
+                    gal_profile_pointer=gal_enet_profile_entity_id,
+                    attributes=extra
+                ).create()
+                results = yield omci.send(frame)
 
-            status = results.fields['omci_message'].fields['success_code']
-            error_mask = results.fields['omci_message'].fields['parameter_error_attributes_mask']
-            self.log.debug('create-gem-port-network-ctp', status=status, error_mask=error_mask)
+                status = results.fields['omci_message'].fields['success_code']
+                error_mask = results.fields['omci_message'].fields['parameter_error_attributes_mask']
+                self.log.debug('create-gem-interworking-tp', status=status, error_mask=error_mask)
 
-        except Exception as e:
-            self.log.exception('gemport-create', e=e)
-            raise
+                if status == ReasonCodes.Success or status == ReasonCodes.InstanceExists:
+                    self._interworking = True
+                else:
+                    raise Exception('GEM Interworking create failed with status: {}'.format(status))
 
-        try:
-            frame = GemInterworkingTpFrame(
-                self.entity_id,          # same entity id as GEM port
-                gem_port_network_ctp_pointer=self.entity_id,
-                interworking_option=5,                             # IEEE 802.1
-                service_profile_pointer=ieee_mapper_service_profile_entity_id,
-                interworking_tp_pointer=0x0,
-                pptp_counter=1,
-                gal_profile_pointer=gal_enet_profile_entity_id
-            ).create()
-            results = yield omci.send(frame)
-
-            status = results.fields['omci_message'].fields['success_code']
-            error_mask = results.fields['omci_message'].fields['parameter_error_attributes_mask']
-            self.log.debug('create-gem-interworking-tp', status=status, error_mask=error_mask)
-
-        except Exception as e:
-            self.log.exception('interworking-create', e=e)
-            raise
+            except Exception as e:
+                self.log.exception('interworking-create', e=e)
+                raise
 
         returnValue(results)
 
     @inlineCallbacks
     def remove_from_hardware(self, omci):
         self.log.debug('remove-from-hardware',  gem_id=self.gem_id)
-        try:
-            frame = GemInterworkingTpFrame(self.entity_id).delete()
-            results = yield omci.send(frame)
 
-            status = results.fields['omci_message'].fields['success_code']
-            self.log.debug('delete-gem-interworking-tp', status=status)
+        results = None
+        if self._interworking:
+            try:
+                frame = GemInterworkingTpFrame(self.entity_id).delete()
+                results = yield omci.send(frame)
+                status = results.fields['omci_message'].fields['success_code']
+                self.log.debug('delete-gem-interworking-tp', status=status)
 
-        except Exception as e:
-            self.log.exception('interworking-delete', e=e)
-            raise
+                if status == ReasonCodes.Success:
+                    self._interworking = False
 
-        try:
-            frame = GemPortNetworkCtpFrame(self.entity_id).delete()
-            results = yield omci.send(frame)
+            except Exception as e:
+                self.log.exception('interworking-delete', e=e)
+                raise
 
-            status = results.fields['omci_message'].fields['success_code']
-            self.log.debug('delete-gem-port-network-ctp', status=status)
+        if self._tcont_entity_id is not None:
+            try:
+                frame = GemPortNetworkCtpFrame(self.entity_id).delete()
+                results = yield omci.send(frame)
 
-        except Exception as e:
-            self.log.exception('gemport-delete', e=e)
-            raise
+                status = results.fields['omci_message'].fields['success_code']
+                self.log.debug('delete-gem-port-network-ctp', status=status)
+
+                if status == ReasonCodes.Success:
+                    self._tcont_entity_id = None
+
+            except Exception as e:
+                self.log.exception('gemport-delete', e=e)
+                raise
 
         returnValue(results)
diff --git a/voltha/adapters/adtran_onu/onu_tcont.py b/voltha/adapters/adtran_onu/onu_tcont.py
index db97810..c94a339 100644
--- a/voltha/adapters/adtran_onu/onu_tcont.py
+++ b/voltha/adapters/adtran_onu/onu_tcont.py
@@ -18,12 +18,14 @@
 from voltha.adapters.adtran_olt.xpon.tcont import TCont
 from voltha.adapters.adtran_olt.xpon.traffic_descriptor import TrafficDescriptor
 from voltha.extensions.omci.omci_me import TcontFrame
-
+from voltha.extensions.omci.omci_defs import ReasonCodes
 
 class OnuTCont(TCont):
     """
     Adtran ONU specific implementation
     """
+    free_tcont_alloc_id = 0xFFFF
+
     def __init__(self, handler, alloc_id, traffic_descriptor, name=None, vont_ani=None):
         super(OnuTCont, self).__init__(alloc_id, traffic_descriptor,
                                        name=name, vont_ani=vont_ani)
@@ -50,15 +52,23 @@
     def add_to_hardware(self, omci, tcont_entity_id):
         self.log.debug('add-to-hardware', tcont_entity_id=tcont_entity_id)
 
-        self._entity_id = tcont_entity_id
+        if self._entity_id == tcont_entity_id:
+            returnValue('Already set')
+
+        elif self.entity_id is not None:
+            raise KeyError('TCONT already assigned: {}'.format(self.entity_id))
 
         try:
-            frame = TcontFrame(self.entity_id, self.alloc_id).set()
+            frame = TcontFrame(tcont_entity_id, self.alloc_id).set()
             results = yield omci.send(frame)
 
             status = results.fields['omci_message'].fields['success_code']
+            if status == ReasonCodes.Success:
+                self._entity_id = tcont_entity_id
+
             failed_attributes_mask = results.fields['omci_message'].fields['failed_attributes_mask']
             unsupported_attributes_mask = results.fields['omci_message'].fields['unsupported_attributes_mask']
+
             self.log.debug('set-tcont', status=status,
                            failed_attributes_mask=failed_attributes_mask,
                            unsupported_attributes_mask=unsupported_attributes_mask)
@@ -72,15 +82,16 @@
     @inlineCallbacks
     def remove_from_hardware(self, omci):
         self.log.debug('remove-from-hardware', tcont_entity_id=self.entity_id)
-
-        # Release tcont by setting alloc_id=0xFFFF
         try:
-            frame = TcontFrame(self.entity_id, 0xFFFF).set()
+            frame = TcontFrame(self.entity_id, OnuTCont.free_tcont_alloc_id).set()
             results = yield omci.send(frame)
 
             status = results.fields['omci_message'].fields['success_code']
             self.log.debug('delete-tcont', status=status)
 
+            if status == ReasonCodes.Success:
+                self._entity_id = None
+
         except Exception as e:
             self.log.exception('tcont-delete', e=e)
             raise