VOL-712: Reboot OMCI Task & Create/Set Tasks for Enable, Disable support

Change-Id: I95253b0f5b6f1c5080ddc36b02ec61e2414051f9
diff --git a/voltha/extensions/omci/me_frame.py b/voltha/extensions/omci/me_frame.py
index c713bc2..7ce812f 100644
--- a/voltha/extensions/omci/me_frame.py
+++ b/voltha/extensions/omci/me_frame.py
@@ -222,19 +222,21 @@
                 attributes_mask=self.entity_class.mask_for(*mask_set)
             ))
 
-    def reboot(self):
+    def reboot(self, reboot_code=0):
         """
         Create a Reboot request from for this ME
         :return: (OmciFrame) OMCI Frame
         """
         self._check_operation(OP.Reboot)
+        assert 0 <= reboot_code <= 2, 'Reboot code must be 0..2'
 
         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')
+                entity_id=getattr(self, 'entity_id'),
+                reboot_code=reboot_code
             ))
 
     def mib_reset(self):
diff --git a/voltha/extensions/omci/omci_cc.py b/voltha/extensions/omci/omci_cc.py
index 6cc05c4..db39abe 100644
--- a/voltha/extensions/omci/omci_cc.py
+++ b/voltha/extensions/omci/omci_cc.py
@@ -648,6 +648,8 @@
     def send_reboot(self, timeout=DEFAULT_OMCI_TIMEOUT):
         """
         Send an ONU Device reboot request (ONU-G ME).
+
+        NOTICE: This method is being deprecated and replaced with a tasks to preform this function
         """
         self.log.debug('send-mib-reboot')
 
diff --git a/voltha/extensions/omci/onu_device_entry.py b/voltha/extensions/omci/onu_device_entry.py
index af60d15..02d7419 100644
--- a/voltha/extensions/omci/onu_device_entry.py
+++ b/voltha/extensions/omci/onu_device_entry.py
@@ -21,6 +21,9 @@
 from common.event_bus import EventBusClient
 from voltha.extensions.omci.tasks.task_runner import TaskRunner
 from voltha.extensions.omci.onu_configuration import OnuConfiguration
+from voltha.extensions.omci.tasks.reboot_task import OmciRebootRequest, RebootFlags
+from voltha.extensions.omci.tasks.omci_modify_request import OmciModifyRequest
+from voltha.extensions.omci.omci_me import OntGFrame
 
 from twisted.internet import reactor
 from enum import IntEnum
@@ -291,6 +294,14 @@
             # Start up the ONU Capabilities task
             self._configuration.reset()
 
+            # Insure that the ONU-G Administrative lock is disabled
+            def failure(reason):
+                self.log.error('disable-admin-state-lock', reason=reason)
+
+            frame = OntGFrame(attributes={'administrative_state': 0}).set()
+            task = OmciModifyRequest(self._omci_agent, self.device_id, frame)
+            self.task_runner.queue_task(task).addErrback(failure)
+
             # Start up any other remaining OpenOMCI state machines
             def start_state_machines(machines):
                 for sm in machines:
@@ -379,3 +390,20 @@
                                                 attributes=attribute)
 
         return entry[attribute] if attribute in entry else None
+
+    def reboot(self,
+               flags=RebootFlags.Reboot_Unconditionally,
+               timeout=OmciRebootRequest.DEFAULT_PRIORITY):
+        """
+        Request a reboot of the ONU
+
+        :param flags: (RebootFlags) Reboot condition
+        :param timeout: (int) Reboot task priority
+        :return: (deferred) Fires upon completion or error
+        """
+        assert self.active, 'This device is not active'
+
+        return self.task_runner.queue_task(OmciRebootRequest(self._omci_agent,
+                                                             self.device_id,
+                                                             flags=flags,
+                                                             timeout=timeout))
diff --git a/voltha/extensions/omci/tasks/omci_get_request.py b/voltha/extensions/omci/tasks/omci_get_request.py
index 7d12829..137f50c 100644
--- a/voltha/extensions/omci/tasks/omci_get_request.py
+++ b/voltha/extensions/omci/tasks/omci_get_request.py
@@ -88,7 +88,7 @@
 
     def stop_if_not_running(self):
         if not self.running:
-            raise MibResyncException('Get Request Task was cancelled')
+            raise GetException('Get Request Task was cancelled')
 
     @property
     def attributes(self):
diff --git a/voltha/extensions/omci/tasks/omci_modify_request.py b/voltha/extensions/omci/tasks/omci_modify_request.py
new file mode 100644
index 0000000..de15011
--- /dev/null
+++ b/voltha/extensions/omci/tasks/omci_modify_request.py
@@ -0,0 +1,176 @@
+#
+# 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 task import Task
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, failure, returnValue
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_me import MEFrame
+from voltha.extensions.omci.omci_frame import OmciFrame
+from voltha.extensions.omci.omci_messages import OmciCreate, OmciSet, OmciDelete
+from voltha.extensions.omci.omci_entities import EntityClass
+
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class ModifyException(Exception):
+    pass
+
+
+class OmciModifyRequest(Task):
+    """
+    OpenOMCI Generic Create, Set, or Delete Frame support Task.
+
+    This task allows an ONU to send a Create, Set, or Delete request from any point in their
+    code while properly using the OMCI-CC channel.  Direct access to the OMCI-CC object
+    to send requests by an ONU is highly discouraged.
+    """
+    task_priority = 128
+    name = "ONU OMCI Modify Task"
+
+    def __init__(self, omci_agent, device_id, frame, priority=task_priority, exclusive=False):
+        """
+        Class initialization
+
+        :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+        :param device_id: (str) ONU Device ID
+        :param frame: (OmciFrame) Frame to send
+        :param priority: (int) OpenOMCI Task priority (0..255) 255 is the highest
+        :param exclusive: (bool) True if this GET request Task exclusively own the
+                                 OMCI-CC while running. Default: False
+        """
+        super(OmciModifyRequest, self).__init__(OmciModifyRequest.name,
+                                                omci_agent,
+                                                device_id,
+                                                priority=priority,
+                                                exclusive=exclusive)
+        self._device = omci_agent.get_device(device_id)
+        self._frame = frame
+        self._results = None
+        self._local_deferred = None
+
+        # Validate message type
+        self._msg_type = frame.fields['message_type']
+        if self._msg_type not in (OmciCreate.message_id, OmciSet.message_id, OmciDelete.message_id):
+            raise TypeError('Invalid Message type: {}, must be Create, Set, or Delete'.
+                            format(self._msg_type))
+
+    def cancel_deferred(self):
+        super(OmciModifyRequest, self).cancel_deferred()
+
+        d, self._local_deferred = self._local_deferred, None
+        try:
+            if d is not None and not d.called:
+                d.cancel()
+        except:
+            pass
+
+    def stop_if_not_running(self):
+        if not self.running:
+            raise ModifyException('Modify Request Task was cancelled')
+
+    @property
+    def success_code(self):
+        """
+        Return the OMCI success/reason code for the Get Response.
+        """
+        if self._results is None:
+            return None
+
+        return self._results.fields['omci_message'].fields['success_code']
+
+    @property
+    def illegal_attributes_mask(self):
+        """
+        For Create & Set requests, a failure may indicate that one or more
+        attributes have an illegal value.  This property returns any illegal
+        attributes
+
+        :return: None if not a create/set request, otherwise the attribute mask
+                 of illegal attributes
+        """
+        if self._results is None:
+            return None
+
+        omci_msg = self._results.fields['omci_message'].fields
+
+        if self._msg_type == OmciCreate.message_id:
+            if self.success_code != RC.ParameterError:
+                return 0
+            return omci_msg['parameter_error_attributes_mask']
+
+        elif self._msg_type == OmciSet.message_id:
+            if self.success_code != RC.AttributeFailure:
+                return 0
+            return omci_msg['failed_attributes_mask']
+
+        return None
+
+    @property
+    def unsupported_attributes_mask(self):
+        """
+        For Set requests, a failure may indicate that one or more attributes
+        are not supported by this ONU. This property returns any those unsupported attributes
+        attributes
+
+        :return: None if not a set request, otherwise the attribute mask of any illegal
+                 parameters
+        """
+        if self._msg_type != OmciSet.message_id or self._results is None:
+            return None
+
+        if self.success_code != RC.AttributeFailure:
+            return 0
+
+        return self._results.fields['omci_message'].fields['unsupported_attributes_mask']
+
+    @property
+    def raw_results(self):
+        """
+        Return the raw Response OMCIFrame
+        """
+        return self._results
+
+    def start(self):
+        """
+        Start MIB Capabilities task
+        """
+        super(OmciModifyRequest, self).start()
+        self._local_deferred = reactor.callLater(0, self.perform_omci)
+
+    @inlineCallbacks
+    def perform_omci(self):
+        """
+        Perform the request
+        """
+        self.log.info('perform-request')
+
+        try:
+            self._results = yield self._device.omci_cc.send(self._frame)
+            self.stop_if_not_running()
+
+            status = self._results.fields['omci_message'].fields['success_code']
+            self.log.info('response-status', status=status)
+
+            # Success?
+            if status in (RC.Success.value, RC.InstanceExists):
+                self.deferred.callback(self)
+            else:
+                raise ModifyException('Failed with status {}'.format(status))
+
+        except Exception as e:
+            self.log.exception('perform-modify', e=e)
+            self.deferred.errback(failure.Failure(e))
diff --git a/voltha/extensions/omci/tasks/reboot_task.py b/voltha/extensions/omci/tasks/reboot_task.py
new file mode 100644
index 0000000..ba4c290
--- /dev/null
+++ b/voltha/extensions/omci/tasks/reboot_task.py
@@ -0,0 +1,122 @@
+#
+# 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 task import Task
+from enum import IntEnum
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, failure, TimeoutError
+from voltha.extensions.omci.omci_defs import ReasonCodes, EntityOperations
+from voltha.extensions.omci.omci_me import OntGFrame
+from voltha.extensions.omci.omci_cc import DEFAULT_OMCI_TIMEOUT
+
+RC = ReasonCodes
+OP = EntityOperations
+
+
+class RebootException(Exception):
+    pass
+
+
+class DeviceBusy(Exception):
+    pass
+
+
+class RebootFlags(IntEnum):
+    Reboot_Unconditionally = 0,
+    Reboot_If_No_POTS_VoIP_In_Progress = 1,
+    Reboot_If_No_Emergency_Call_In_Progress = 2
+
+
+class OmciRebootRequest(Task):
+    """
+    OpenOMCI routine to request reboot of an ONU
+    """
+    task_priority = Task.MAX_PRIORITY
+    name = "ONU OMCI Reboot Task"
+
+    def __init__(self, omci_agent, device_id,
+                 flags=RebootFlags.Reboot_Unconditionally,
+                 timeout=DEFAULT_OMCI_TIMEOUT):
+        """
+        Class initialization
+
+        :param omci_agent: (OmciAdapterAgent) OMCI Adapter agent
+        :param flags: (RebootFlags) Reboot condition
+        """
+        super(OmciRebootRequest, self).__init__(OmciRebootRequest.name,
+                                                omci_agent,
+                                                device_id,
+                                                priority=OmciRebootRequest.task_priority,
+                                                exclusive=True)
+        self._device = omci_agent.get_device(device_id)
+        self._flags = flags
+        self._timeout = timeout
+        self._local_deferred = None
+
+    def cancel_deferred(self):
+        super(OmciRebootRequest, self).cancel_deferred()
+
+        d, self._local_deferred = self._local_deferred, None
+        try:
+            if d is not None and not d.called:
+                d.cancel()
+        except:
+            pass
+
+    def start(self):
+        """ Start task """
+        super(OmciRebootRequest, self).start()
+        self._local_deferred = reactor.callLater(0, self.perform_reboot)
+
+    @inlineCallbacks
+    def perform_reboot(self):
+        """
+        Perform the reboot requests
+
+        Depending on the ONU implementation, a response may not be returned. For this
+        reason, a timeout is considered successful.
+        """
+        self.log.info('perform-reboot')
+
+        try:
+            frame = OntGFrame().reboot(reboot_code=self._flags)
+            results = yield self._device.omci_cc.send(frame, timeout=self._timeout)
+
+            status = results.fields['omci_message'].fields['success_code']
+            self.log.debug('reboot-status', status=status)
+
+            # Did it fail
+            if status != RC.Success.value:
+                if self._flags != RebootFlags.Reboot_Unconditionally and\
+                        status == RC.DeviceBusy.value:
+                    raise DeviceBusy('ONU is busy, try again later')
+                else:
+                    msg = 'Reboot request failed with status {}'.format(status)
+                    raise RebootException(msg)
+
+            self.log.info('reboot-success')
+            self.deferred.callback(None)
+
+        except TimeoutError:
+            self.log.info('timeout', msg='Request timeout is not considered an error')
+            self.deferred.callback(None)
+
+        except DeviceBusy as e:
+            self.log.warn('perform-reboot', msg=e)
+            self.deferred.errback(failure.Failure(e))
+
+        except Exception as e:
+            self.log.exception('perform-reboot', e=e)
+            self.deferred.errback(failure.Failure(e))
diff --git a/voltha/extensions/omci/tasks/task.py b/voltha/extensions/omci/tasks/task.py
index 3bae4fc..9e10c65 100644
--- a/voltha/extensions/omci/tasks/task.py
+++ b/voltha/extensions/omci/tasks/task.py
@@ -33,6 +33,8 @@
     Failure object.
     """
     DEFAULT_PRIORITY = 128
+    MIN_PRIORITY = 0
+    MAX_PRIORITY = 255
     _next_task_id = 0
 
     def __init__(self, name, omci_agent, device_id, priority=DEFAULT_PRIORITY,
@@ -46,7 +48,8 @@
         :param exclusive: (bool) If True, this task needs exclusive access to the
                                  OMCI Communications channel when it runs
         """
-        assert 0 <= priority <= 255, 'Priority should be 0..255'
+        assert Task.MIN_PRIORITY <= priority <= Task.MAX_PRIORITY, \
+            'Priority should be {}..{}'.format(Task.MIN_PRIORITY, Task.MAX_PRIORITY)
 
         Task._next_task_id += 1
         self._task_id = Task._next_task_id