VOL-618: ADTRAN ONU support for OMCI remote reboot

Change-Id: Icd332bcddf5f1224d5ba4050efaf1b2bcff602d9
diff --git a/voltha/adapters/adtran_onu/adtran_onu.py b/voltha/adapters/adtran_onu/adtran_onu.py
index c0d9ae9..b062aa0 100755
--- a/voltha/adapters/adtran_onu/adtran_onu.py
+++ b/voltha/adapters/adtran_onu/adtran_onu.py
@@ -36,7 +36,7 @@
                                                device_handler_class=AdtranOnuHandler,
                                                name='adtran_onu',
                                                vendor='Adtran, Inc.',
-                                               version='0.4',
+                                               version='0.5',
                                                device_type='adtran_onu',
                                                vendor_id='ADTN')
 
diff --git a/voltha/adapters/adtran_onu/adtran_onu_handler.py b/voltha/adapters/adtran_onu/adtran_onu_handler.py
index 2231dd4..dea1e78 100644
--- a/voltha/adapters/adtran_onu/adtran_onu_handler.py
+++ b/voltha/adapters/adtran_onu/adtran_onu_handler.py
@@ -39,7 +39,8 @@
 
 _ = third_party
 _MAXIMUM_PORT = 128          # PON and UNI ports
-
+_ONU_REBOOT_MIN = 60
+_ONU_REBOOT_RETRY = 10
 
 class AdtranOnuHandler(AdtranXPON):
     def __init__(self, adapter, device_id):
@@ -544,41 +545,88 @@
 
     @inlineCallbacks
     def reboot(self):
-        from common.utils.asleep import asleep
         self.log.info('rebooting', device_id=self.device_id)
         self._cancel_deferred()
 
-        # Drop registration for adapter messages
-        self.adapter_agent.unregister_for_inter_adapter_messages()
+        reregister = True
+        try:
+            # Drop registration for adapter messages
+            self.adapter_agent.unregister_for_inter_adapter_messages()
+
+        except KeyError:
+            reregister = False
 
         # Update the operational status to ACTIVATING and connect status to
         # UNREACHABLE
         device = self.adapter_agent.get_device(self.device_id)
+
         previous_oper_status = device.oper_status
         previous_conn_status = device.connect_status
+
         device.oper_status = OperStatus.ACTIVATING
         device.connect_status = ConnectStatus.UNREACHABLE
-        device.reason = 'Rebooting'
-
+        device.reason = 'Attempting reboot'
         self.adapter_agent.update_device(device)
 
-        # Sleep 10 secs, simulating a reboot
         # TODO: send alert and clear alert after the reboot
-        yield asleep(10)    # TODO: Need to reboot for real
 
-        # Register for adapter messages
-        self.adapter_agent.register_for_inter_adapter_messages()
+        if not self.is_mock:
+            from twisted.internet.defer import TimeoutError
+
+            try:
+                ######################################################
+                # MIB Reset - For ADTRAN ONU, we do not get a response
+                #             back (because we are rebooting)
+                pass
+                yield self.omci.send_reboot(timeout=0.1)
+
+            except TimeoutError:
+                # This is expected
+                returnValue('reboot-in-progress')
+
+            except Exception as e:
+                self.log.exception('send-reboot', e=e)
+                raise
+
+        # Reboot in progress. A reboot may take up to 3 min 30 seconds
+        # Go ahead and pause less than that and start to look
+        # for it being alive
+
+        device.reason = 'reboot in progress'
+        self.adapter_agent.update_device(device)
+
+        self._deferred = reactor.callLater(_ONU_REBOOT_MIN,
+                                           self._finish_reboot,
+                                           previous_oper_status,
+                                           previous_conn_status,
+                                           reregister)
+
+    @inlineCallbacks
+    def _finish_reboot(self, previous_oper_status, previous_conn_status,
+                       reregister):
+        from common.utils.asleep import asleep
+
+        if not self.is_mock:
+            # TODO: Do a simple poll and call this again if we timeout
+            # _ONU_REBOOT_RETRY
+            yield asleep(180)       # 3 minutes ...
 
         # Change the operational status back to its previous state.  With a
         # real OLT the operational state should be the state the device is
         # after a reboot.
         # Get the latest device reference
         device = self.adapter_agent.get_device(self.device_id)
+
         device.oper_status = previous_oper_status
         device.connect_status = previous_conn_status
         device.reason = ''
         self.adapter_agent.update_device(device)
-        self.log.info('rebooted', device_id=self.device_id)
+
+        if reregister:
+            self.adapter_agent.register_for_inter_adapter_messages()
+
+        self.log.info('reboot-complete', device_id=self.device_id)
+
 
     def self_test_device(self, device):
         """
@@ -699,17 +747,38 @@
 
     def delete(self):
         self.log.info('deleting', device_id=self.device_id)
-        # A delete request may be received when an OLT is disabled
-
-        self.enabled = False
-
-        # TODO:  Need to implement this
-        # 1) Remove all flows from the device
-
-        self.log.info('deleted', device_id=self.device_id)
-
-        # Drop device ID
-        self.device_id = None
+        #
+        # handling needed here
+        # self.enabled = False
+        #
+        # # TODO:  Need to implement this
+        # # 1) Remove all flows from the device
+        #
+        # self.log.info('deleted', device_id=self.device_id)
+        #
+        # # Drop device ID
+        # self.device_id = None    @inlineCallbacks
+    # def delete_v_ont_ani(self, data):
+    #     self.log.info('deleting-v_ont_ani')
+    #
+    #     device = self.adapter_agent.get_device(self.device_id)
+    #     # construct message
+    #     # MIB Reset - OntData - 0
+    #     if device.connect_status != ConnectStatus.REACHABLE:
+    #         self.log.error('device-unreachable')
+    #         returnValue(None)
+    #
+    #     self.send_mib_reset()
+    #     yield self.wait_for_response()
+    #     self.proxy_address = device.proxy_address
+    #     self.adapter_agent.unregister_for_proxied_messages(device.proxy_address)
+    #
+    #     ports = self.adapter_agent.get_ports(self.device_id, Port.PON_ONU)
+    #     if ports is not None:
+    #         for port in ports:
+    #             if port.label == 'PON port':
+    #                 self.adapter_agent.delete_port(self.device_id, port)
+    #                 break
 
     def _check_for_mock_config(self, data):
         # Check for MOCK configuration
@@ -799,8 +868,7 @@
         return update
 
     def on_vont_ani_delete(self, vont_ani):
-        # TODO: Is this ever called or is the iAdapter 'delete' called first?
-        return None   # Implement in your OLT, if needed
+        return self.delete()
 
     def on_venet_create(self, venet):
         self.log.info('venet-create', venet=venet)
diff --git a/voltha/adapters/adtran_onu/omci/me_frame.py b/voltha/adapters/adtran_onu/omci/me_frame.py
index bac4c33..1808d04 100644
--- a/voltha/adapters/adtran_onu/omci/me_frame.py
+++ b/voltha/adapters/adtran_onu/omci/me_frame.py
@@ -51,7 +51,7 @@
 
     @property
     def entity_class_name(self):
-        return self._class.__class__.__name__
+        return self._class.__name__
 
     @property
     def entity_id(self):
@@ -64,23 +64,34 @@
     @staticmethod
     def check_type(param, types):
         if not isinstance(param, types):
-            raise TypeError("param '{}' should be a {}".format(param, types))
+            raise TypeError("Parameter '{}' should be a {}".format(param, types))
 
     def _check_operation(self, operation):
         allowed = self.entity_class.mandatory_operations | self.entity_class.optional_operations
-        assert operation in allowed, "{} not allowed for '{}'".format(str(operation).split('.')[1],
+        assert operation in allowed, "{} not allowed for '{}'".format(operation.name,
                                                                       self.entity_class_name)
 
     def _check_attributes(self, attributes, access):
-        for attribute in attributes:
-            index = self.entity_class.attribute_name_to_index_map.get(attribute)
+        keys = attributes.keys() if isinstance(attributes, dict) else attributes
+        for attr_name in keys:
             # Bad attribute name (invalid or spelling error)?
-            assert index is not None, "Attribute '{}' is not valid for '{}'".format(attribute,
-                                                                                    self.entity_class_name)
+            index = self.entity_class.attribute_name_to_index_map.get(attr_name)
+            if index is None:
+                raise KeyError("Attribute '{}' is not valid for '{}'".
+                               format(attr_name, self.entity_class_name))
             # Invalid access?
-            # TODO: Add read-only access to EntityClass _access set. Currently it is protected
-            assert access in self.entity_class.attributes[index]._access,\
-                "No '{} access for attribute '{}".format(str(access).split('.')[1], attribute)
+            assert access in self.entity_class.attributes[index].access, \
+                "Access '{}' for attribute '{}' is not valid for '{}'".format(access.name,
+                                                                              attr_name,
+                                                                              self.entity_class_name)
+
+        if access.value in [AA.W.value, AA.SBC.value] and isinstance(attributes, dict):
+            for attr_name, value in attributes.iteritems():
+                index = self.entity_class.attribute_name_to_index_map.get(attr_name)
+                attribute = self.entity_class.attributes[index]
+                if not attribute.valid(value):
+                    raise ValueError("Invalid value '{}' for attribute '{}' of '{}".
+                                     format(value, attr_name, self.entity_class_name))
 
     @staticmethod
     def _attr_to_data(attributes):
@@ -138,7 +149,7 @@
         assert len(data) > 0, 'No attributes supplied'
 
         self._check_operation(OP.Create)
-        self._check_attributes(data.keys(), AA.Writable)
+        self._check_attributes(data, AA.Writable)
 
         return OmciFrame(
             transaction_id=None,
@@ -175,7 +186,7 @@
         assert len(data) > 0, 'No attributes supplied'
 
         self._check_operation(OP.Set)
-        self._check_attributes(data.keys(), AA.Writable)
+        self._check_attributes(data, AA.Writable)
 
         return OmciFrame(
             transaction_id=None,
@@ -210,3 +221,71 @@
                 entity_id=getattr(self, 'entity_id'),
                 attributes_mask=self.entity_class.mask_for(*mask_set)
             ))
+
+    def reboot(self):
+        """
+        Create a Reboot request from for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        self._check_operation(OP.Reboot)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciReboot.message_id,
+            omci_message=OmciReboot(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id')
+            ))
+
+    def mib_reset(self):
+        """
+        Create a MIB Reset request from for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        self._check_operation(OP.MibReset)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciMibReset.message_id,
+            omci_message=OmciMibReset(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id')
+            ))
+
+    def mib_upload(self):
+        """
+        Create a MIB Upload request from for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        self._check_operation(OP.MibUpload)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciMibUpload.message_id,
+            omci_message=OmciMibUpload(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id')
+            ))
+
+    def mib_upload_next(self):
+        """
+        Create a MIB Upload Next request from for this ME
+        :return: (OmciFrame) OMCI Frame
+        """
+        assert hasattr(self, 'data'), 'data required for Set actions'
+        data = getattr(self, 'data')
+        MEFrame.check_type(data, dict)
+        assert len(data) > 0, 'No attributes supplied'
+        assert 'mib_data_sync' in data, "'mib_data_sync' not in attributes list"
+
+        self._check_operation(OP.MibUploadNext)
+        self._check_attributes(data, AA.Writable)
+
+        return OmciFrame(
+            transaction_id=None,
+            message_type=OmciMibUploadNext.message_id,
+            omci_message=OmciMibUploadNext(
+                entity_class=getattr(self.entity_class, 'class_id'),
+                entity_id=getattr(self, 'entity_id'),
+                command_sequence_number=data['mib_data_sync']
+            ))
diff --git a/voltha/adapters/adtran_onu/omci/omci_cc.py b/voltha/adapters/adtran_onu/omci/omci_cc.py
index 9ee5a64..413a055 100644
--- a/voltha/adapters/adtran_onu/omci/omci_cc.py
+++ b/voltha/adapters/adtran_onu/omci/omci_cc.py
@@ -26,6 +26,7 @@
 from common.frameio.frameio import hexify
 from voltha.extensions.omci.omci import *
 from omci_entities import add_onu_me_entities
+from omci_me import OntGFrame, OntDataFrame
 
 _ = third_party
 
@@ -167,6 +168,9 @@
         Attempt to retrieve and remove an ONU Alarm Message from the ONU
         autonomous message queue.
 
+        TODO: We may want to deprecate this, see TODO comment around line 399 in
+              the _request_success() method below
+
         :return: a Deferred which fires with the next Alarm Frame available in
                  the queue.
         """
@@ -178,6 +182,9 @@
         Attempt to retrieve and remove an ONU Attribute Value Change (AVC)
         Message from the ONU autonomous message queue.
 
+        TODO: We may want to deprecate this, see TODO comment around line 399 in
+              the _request_success() method below
+
         :return: a Deferred which fires with the next AVC Frame available in
                  the queue.
         """
@@ -189,6 +196,9 @@
         Attempt to retrieve and remove an ONU Test Results Message from the
         ONU autonomous message queue.
 
+        TODO: We may want to deprecate this, see TODO comment around line 399 in
+              the _request_success() method below
+
         :return: a Deferred which fires with the next Test Results Frame is
                  available in the queue.
         """
@@ -278,7 +288,7 @@
                     self._consecutive_errors = 0
 
                 except KeyError as e:
-                    # TODO: Investigate.  Probably an unknown/unsupported ME
+                    # Unknown, Unsupported, or vendor-specific ME. Key is the unknown classID
                     # TODO: Can we create a temporary one to hold it so upload does not always fail on new ME's?
                     self.log.exception('frame-decode-key-error', msg=hexlify(msg), e=e)
                     return
@@ -384,6 +394,13 @@
         # TODO: Here we could update the MIB database if we did a set/create/delete
         #       or perhaps a verify if a GET.  Also could increment mib counter
         #
+        # TODO: A better way to perform this in VOLTHA v1.3 would be to provide
+        #       a pub/sub capability for external users/tasks to monitor responses
+        #       that could optionally take a filter. This would allow a MIB-Sync
+        #       task to easily watch all AVC notifications as well as Set/Create/Delete
+        #       operations and keep them serialized.  It may also be a better/easier
+        #       way to handle things if we containerize OpenOMCI.
+        #
         try:
             if isinstance(rx_frame.omci_message, OmciGetResponse):
                 pass    # TODO: Implement MIB check or remove
@@ -454,42 +471,34 @@
         return d
 
     ###################################################################################
-    # TODO: The following three need to be ported to the new OMCI_CC and ME_Frame style
-    #       or perhaps made into static methods in the base ME_Frame class.
+    # MIB Action shortcuts
 
-    def send_mib_reset(self, entity_id=0, timeout=DEFAULT_OMCI_TIMEOUT):
+    def send_mib_reset(self, timeout=DEFAULT_OMCI_TIMEOUT):
+        """
+        Perform a MIB Reset
+        """
         self.log.debug('send-mib-reset')
-        frame = OmciFrame(
-            transaction_id=self._get_tx_tid(),
-            message_type=OmciMibReset.message_id,
-            omci_message=OmciMibReset(
-                entity_class=OntData.class_id,
-                entity_id=entity_id
-            )
-        )
+
+        frame = OntDataFrame().mib_reset()
         return self.send(frame, timeout)
 
     def send_mib_upload(self, timeout=DEFAULT_OMCI_TIMEOUT):
         self.log.debug('send-mib-upload')
-        frame = OmciFrame(
-            transaction_id=self._get_tx_tid(),
-            message_type=OmciMibUpload.message_id,
-            omci_message=OmciMibUpload(
-                entity_class=OntData.class_id,
-                entity_id=0
-            )
-        )
+
+        frame = OntDataFrame().mib_upload()
         return self.send(frame, timeout)
 
     def send_mib_upload_next(self, seq_no, timeout=DEFAULT_OMCI_TIMEOUT):
         self.log.debug('send-mib-upload-next')
-        frame = OmciFrame(
-            transaction_id=self._get_tx_tid(),
-            message_type=OmciMibUploadNext.message_id,
-            omci_message=OmciMibUploadNext(
-                entity_class=OntData.class_id,
-                entity_id=0,
-                command_sequence_number=seq_no
-            )
-        )
+
+        frame = OntDataFrame(seq_no).mib_upload_next()
+        return self.send(frame, timeout)
+
+    def send_reboot(self, timeout=DEFAULT_OMCI_TIMEOUT):
+        """
+        Send an ONU Device reboot request (ONU-G ME).
+        """
+        self.log.debug('send-mib-reboot')
+
+        frame = OntGFrame().reboot()
         return self.send(frame, timeout)
diff --git a/voltha/adapters/adtran_onu/omci/omci_entities.py b/voltha/adapters/adtran_onu/omci/omci_entities.py
index f38f55b..62c8516 100644
--- a/voltha/adapters/adtran_onu/omci/omci_entities.py
+++ b/voltha/adapters/adtran_onu/omci/omci_entities.py
@@ -70,7 +70,7 @@
     mandatory_operations = {OP.Get}
 
 
-class FlexibleConfigurationStatusPortal(EntityClass):
+class FCPortalOrSraStat(EntityClass):
     class_id = 65420
     attributes = [
         ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
@@ -78,10 +78,9 @@
     mandatory_operations = {OP.Get, OP.Set, OP.Create, OP.Delete}
 
 
-class ONU3G(EntityClass):
+class Onu3gOrInvStat2(EntityClass):
     class_id = 65422
     attributes = [
-        # TODO: Fix access for all attributes below
         ECA(ShortField("managed_entity_id", None), {AA.R, AA.SBC}),
     ]
     mandatory_operations = {OP.Set, OP.Get, OP.Create, OP.Delete}
diff --git a/voltha/adapters/adtran_onu/omci/omci_me.py b/voltha/adapters/adtran_onu/omci/omci_me.py
index c3716cf..6b26ad5 100644
--- a/voltha/adapters/adtran_onu/omci/omci_me.py
+++ b/voltha/adapters/adtran_onu/omci/omci_me.py
@@ -494,7 +494,7 @@
     """
     This managed entity represents the ONU as equipment.
     """
-    def __init__(self, attributes):
+    def __init__(self, attributes=None):
         """
         :param attributes: (basestring, list, set, dict) attributes. For gets
                            a string, list, or set can be provided. For create/set
@@ -509,7 +509,7 @@
     """
     This managed entity contains additional attributes associated with a PON ONU.
     """
-    def __init__(self, attributes):
+    def __init__(self, attributes=None):
         """
         :param attributes: (basestring, list, set, dict) attributes. For gets
                            a string, list, or set can be provided. For create/set
@@ -643,3 +643,22 @@
         super(VlanTaggingFilterDataFrame, self).__init__(VlanTaggingFilterData,
                                                          entity_id,
                                                          data)
+
+
+class OntDataFrame(MEFrame):
+    """
+    This managed entity models the MIB itself
+    """
+    def __init__(self, mib_data_sync=None):
+        """
+        :param mib_data_sync: (int) This attribute is used to check the alignment
+                                    of the MIB of the ONU with the corresponding MIB
+                                    in the OLT. (0..255)
+        """
+        self.check_type(mib_data_sync, (int, type(None)))
+        if mib_data_sync is not None and not 0 <= mib_data_sync <= 255:
+            raise ValueError('mib_data_sync should be 0..255')
+
+        data = {'mib_data_sync': mib_data_sync} if mib_data_sync is not None else None
+
+        super(OntDataFrame, self).__init__(OntData, 0, data)
diff --git a/voltha/adapters/adtran_onu/pon_port.py b/voltha/adapters/adtran_onu/pon_port.py
index 4b6c044..b9c82ba 100644
--- a/voltha/adapters/adtran_onu/pon_port.py
+++ b/voltha/adapters/adtran_onu/pon_port.py
@@ -338,6 +338,8 @@
             returnValue('not-enabled')
 
         device = None
+        seq_no = 0
+        number_of_commands = 0
         omci = self._handler.omci
 
         if self._handler.is_mock:
@@ -354,7 +356,6 @@
             results = yield omci.send_mib_reset()
             status = results.fields['omci_message'].fields['success_code']
             assert status == 0, 'Unexpected MIB reset response status: {}'.format(status)
-
             # TODO: On a real system, need to flush the external MIB database
             # TODO: Also would need to watch for any AVC being handled between the MIB reset and the DB flush
             self.mib_data_store = dict()
@@ -380,7 +381,6 @@
                     pass
 
             # Successful if here
-
             device.reason = 'MIB Synchronization Complete'
             self._handler.adapter_agent.update_device(device)
 
@@ -389,7 +389,7 @@
             self.log.info('mib-synchronized')
 
         except TimeoutError as e:
-            self.log.warn('mib-upload', e=e)
+            self.log.warn('mib-upload', e=e, seq_no=seq_no, number_of_commands=number_of_commands)
 
             if device is not None:
                 device.reason = 'mib-upload-failure: Response Timeout'