VOL-607: OpenOMCI OMCI_CC and MEFrame class support
Fixed bug found during unit test create
Change-Id: Ib939681ebf81618af95b3597c3311526626aaebf
diff --git a/tests/utests/voltha/extensions/omci/test_me_frame.py b/tests/utests/voltha/extensions/omci/test_me_frame.py
new file mode 100644
index 0000000..07aa49d
--- /dev/null
+++ b/tests/utests/voltha/extensions/omci/test_me_frame.py
@@ -0,0 +1,45 @@
+#
+# 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 unittest import TestCase, main
+
+from voltha.extensions.omci.me_frame import *
+
+# NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE
+#
+# NOTE: This is a placeholder for OpenOMCI unit tests of the MEFrame class
+# Initial (worthwhile) tests will be provided in VOL-607
+#
+# NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE
+
+
+class TestMeFrameExample(TestCase):
+
+ def test_example_1(self):
+
+ self.assertTrue(True)
+ self.assertFalse(False)
+ self.assertEqual('123', '123')
+
+ def test_example_3(self):
+
+ self.assertTrue(True)
+ self.assertFalse(False)
+ self.assertEqual('123', '123')
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/tests/utests/voltha/extensions/omci/test_omci_cc.py b/tests/utests/voltha/extensions/omci/test_omci_cc.py
new file mode 100644
index 0000000..e68c61c
--- /dev/null
+++ b/tests/utests/voltha/extensions/omci/test_omci_cc.py
@@ -0,0 +1,47 @@
+#
+# 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 unittest import TestCase, main
+
+from voltha.extensions.omci.omci_cc import *
+
+
+# NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE
+#
+# NOTE: This is a placeholder for OpenOMCI unit tests of the OMCI_CC class
+# Initial (worthwhile) tests will be provided in VOL-607. The VOL-607
+# check-in will also likely include the start of a mock ONU device.
+#
+# NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE
+
+
+class TestOmciCcExample(TestCase):
+
+ def test_example_1(self):
+
+ self.assertTrue(True)
+ self.assertFalse(False)
+ self.assertEqual('123', '123')
+
+ def test_example_3(self):
+
+ self.assertTrue(True)
+ self.assertFalse(False)
+ self.assertEqual('123', '123')
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/voltha/extensions/omci/me_frame.py b/voltha/extensions/omci/me_frame.py
new file mode 100644
index 0000000..1808d04
--- /dev/null
+++ b/voltha/extensions/omci/me_frame.py
@@ -0,0 +1,291 @@
+#
+# 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.
+#
+"""
+OMCI Managed Entity Message support base class
+"""
+from voltha.extensions.omci.omci import *
+
+# abbreviations
+OP = EntityOperations
+AA = AttributeAccess
+
+
+class MEFrame(object):
+ """Base class to help simplify Frame Creation"""
+ def __init__(self, entity_class, entity_id, data):
+ assert issubclass(entity_class, EntityClass), \
+ "'{}' must be a subclass of MEFrame".format(entity_class)
+ self.check_type(entity_id, int)
+
+ if not 0 <= entity_id <= 0xFFFF:
+ raise ValueError('entity_id should be 0..65535')
+
+ self._class = entity_class
+ self._entity_id = entity_id
+ self.data = data
+
+ def __str__(self):
+ return '{}: Entity_ID: {}, Data: {}'.\
+ format(self.entity_class_name, self._entity_id, self.data)
+
+ @property
+ def entity_class(self):
+ """
+ The Entity Class for this ME
+ :return: (EntityClass) Entity class
+ """
+ return self._class
+
+ @property
+ def entity_class_name(self):
+ return self._class.__name__
+
+ @property
+ def entity_id(self):
+ """
+ The Entity ID for this ME frame
+ :return: (int) Entity ID (0..0xFFFF)
+ """
+ return self._entity_id
+
+ @staticmethod
+ def check_type(param, types):
+ if not isinstance(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(operation.name,
+ self.entity_class_name)
+
+ def _check_attributes(self, attributes, access):
+ keys = attributes.keys() if isinstance(attributes, dict) else attributes
+ for attr_name in keys:
+ # Bad attribute name (invalid or spelling error)?
+ 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?
+ 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):
+ """
+ Convert an object into the 'data' set or dictionary for get/set/create/delete
+ requests.
+
+ This method takes a 'string', 'list', or 'set' for get requests and
+ converts it to a 'set' of attributes.
+
+ For create/set requests a dictionary of attribute/value pairs is required
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, set, or dict can be provided. For create/set
+ operations, a dictionary should be provided. For delete
+ the attributes may be None since they are ignored.
+
+ :return: (set, dict) set for get/deletes, dict for create/set
+ """
+ if isinstance(attributes, basestring):
+ # data = [str(attributes)]
+ data = set()
+ data.add(str(attributes))
+
+ elif isinstance(attributes, list):
+ assert all(isinstance(attr, basestring) for attr in attributes),\
+ 'attribute list must be strings'
+ data = {str(attr) for attr in attributes}
+ assert len(data) == len(attributes), 'Attributes were not unique'
+
+ elif isinstance(attributes, set):
+ assert all(isinstance(attr, basestring) for attr in attributes),\
+ 'attribute set must be strings'
+ data = {str(attr) for attr in attributes}
+
+ elif isinstance(attributes, (dict, type(None))):
+ data = attributes
+
+ else:
+ raise TypeError("Unsupported attributes type '{}'".format(type(attributes)))
+
+ return data
+
+ def create(self):
+ """
+ Create a Create request frame for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ assert hasattr(self.entity_class, 'class_id'), 'class_id required for Create actions'
+ assert hasattr(self, 'entity_id'), 'entity_id required for Create actions'
+ assert hasattr(self, 'data'), 'data required for Create actions'
+
+ data = getattr(self, 'data')
+ MEFrame.check_type(data, dict)
+ assert len(data) > 0, 'No attributes supplied'
+
+ self._check_operation(OP.Create)
+ self._check_attributes(data, AA.Writable)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciCreate.message_id,
+ omci_message=OmciCreate(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ data=data
+ ))
+
+ def delete(self):
+ """
+ Create a Delete request frame for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ self._check_operation(OP.Delete)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciGet.message_id,
+ omci_message=OmciGet(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id')
+ ))
+
+ def set(self):
+ """
+ Create a Set request frame 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'
+
+ self._check_operation(OP.Set)
+ self._check_attributes(data, AA.Writable)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciSet.message_id,
+ omci_message=OmciSet(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ entity_id=getattr(self, 'entity_id'),
+ attributes_mask=self.entity_class.mask_for(*data.keys()),
+ data=data
+ ))
+
+ def get(self):
+ """
+ Create a Get request frame for this ME
+ :return: (OmciFrame) OMCI Frame
+ """
+ assert hasattr(self, 'data'), 'data required for Get actions'
+ data = getattr(self, 'data')
+ MEFrame.check_type(data, (list, set, dict))
+ assert len(data) > 0, 'No attributes supplied'
+
+ mask_set = data.keys() if isinstance(data, dict) else data
+
+ self._check_operation(OP.Get)
+ self._check_attributes(mask_set, AA.Readable)
+
+ return OmciFrame(
+ transaction_id=None,
+ message_type=OmciGet.message_id,
+ omci_message=OmciGet(
+ entity_class=getattr(self.entity_class, 'class_id'),
+ 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/extensions/omci/omci_cc.py b/voltha/extensions/omci/omci_cc.py
new file mode 100644
index 0000000..8a5c55c
--- /dev/null
+++ b/voltha/extensions/omci/omci_cc.py
@@ -0,0 +1,519 @@
+#
+# 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.
+#
+"""
+OMCI Message support
+"""
+
+import sys
+import arrow
+from twisted.internet import reactor, defer
+from twisted.internet.defer import DeferredQueue, TimeoutError, CancelledError, failure, fail
+from voltha.protos import third_party
+from common.frameio.frameio import hexify
+from voltha.extensions.omci.omci import *
+from voltha.extensions.omci.omci_me import OntGFrame, OntDataFrame
+
+_ = third_party
+
+_MAX_INCOMING_ALARM_MESSAGES = 256
+_MAX_INCOMING_AVC_MESSAGES = 256
+_MAX_INCOMING_TEST_RESULT_MESSAGES = 64
+
+DEFAULT_OMCI_TIMEOUT = 3 # Seconds
+MAX_OMCI_REQUEST_AGE = 60 # Seconds
+MAX_OMCI_TX_ID = 0xFFFF # 2 Octets max
+
+# abbreviations
+OP = EntityOperations
+
+
+class OMCI_CC(object):
+ """ Handle OMCI Communication Channel specifics for Adtran ONUs"""
+
+ def __init__(self, adapter_agent, device_id, me_map=None,
+ alarm_queue_limit=_MAX_INCOMING_ALARM_MESSAGES,
+ avc_queue_limit=_MAX_INCOMING_ALARM_MESSAGES,
+ test_results_queue_limit=_MAX_INCOMING_TEST_RESULT_MESSAGES):
+ self.log = structlog.get_logger(device_id=device_id)
+ self._adapter_agent = adapter_agent
+ self._device_id = device_id
+ self._proxy_address = None
+ self._tx_tid = 1
+ self._enabled = False
+ self._requests = dict() # Tx ID -> (timestamp, deferred, tx_frame, timeout)
+ self._alarm_queue = DeferredQueue(size=alarm_queue_limit)
+ self._avc_queue = DeferredQueue(size=avc_queue_limit)
+ self._test_results_queue = DeferredQueue(size=test_results_queue_limit)
+ self._me_map = me_map
+
+ # Statistics
+ self._tx_frames = 0
+ self._rx_frames = 0
+ self._rx_unknown_tid = 0 # Rx OMCI with no Tx TID match
+ self._rx_onu_frames = 0 # Autonomously generated ONU frames
+ self._rx_alarm_overflow = 0 # Autonomously generated ONU alarms rx overflow
+ self._rx_avc_overflow = 0 # Autonomously generated ONU AVC rx overflow
+ self._rx_onu_discards = 0 # Autonomously generated ONU unknown message types
+ self._rx_timeouts = 0
+ self._tx_errors = 0 # Exceptions during tx request
+ self._consecutive_errors = 0 # Rx & Tx errors in a row, a good RX resets this to 0
+ self._reply_min = sys.maxint # Fastest successful tx -> rx
+ self._reply_max = 0 # Longest successful tx -> rx
+ self._reply_sum = 0.0 # Total seconds for successful tx->rx (float for average)
+
+ # If a list of custom ME Entities classes were provided, insert them into
+ # main class_id to entity map.
+ # TODO: If this class becomes hidden from the ONU DA, move this to the OMCI State Machine runner
+
+ def __str__(self):
+ return "OMCISupport: {}".format(self._device_id)
+
+ @property
+ def enabled(self):
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, value):
+ """
+ Enable/disable the OMCI Communications Channel
+
+ :param value: (boolean) True to enable, False to disable
+ """
+ assert isinstance(value, bool), 'enabled is a boolean'
+
+ if self._enabled != value:
+ self._enabled = value
+ if self._enabled:
+ self._start()
+ else:
+ self._stop()
+
+ @property
+ def tx_frames(self):
+ return self._tx_frames
+
+ @property
+ def rx_frames(self):
+ return self._rx_frames
+
+ @property
+ def rx_unknown_tid(self):
+ return self._rx_unknown_tid # Tx TID not found
+
+ @property
+ def rx_onu_frames(self):
+ return self._rx_onu_frames
+
+ @property
+ def rx_alarm_overflow(self):
+ return self._rx_alarm_overflow # Alarm ONU autonomous overflows
+
+ @property
+ def rx_avc_overflow(self):
+ return self._rx_avc_overflow # Attribute Value change autonomous overflows
+
+ @property
+ def rx_onu_discards(self):
+ return self._rx_onu_discards # Attribute Value change autonomous overflows
+
+ @property
+ def rx_timeouts(self):
+ return self._rx_timeouts
+
+ @property
+ def tx_errors(self):
+ return self._tx_errors
+
+ @property
+ def consecutive_errors(self):
+ return self._consecutive_errors
+
+ @property
+ def reply_min(self):
+ return int(round(self._reply_min * 1000.0)) # Milliseconds
+
+ @property
+ def reply_max(self):
+ return int(round(self._reply_max * 1000.0)) # Milliseconds
+
+ @property
+ def reply_average(self):
+ avg = self._reply_sum / self._rx_frames if self._rx_frames > 0 else 0.0
+ return int(round(avg * 1000.0)) # Milliseconds
+
+ @property
+ def get_alarm_message(self):
+ """
+ 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.
+ """
+ return self._alarm_queue.get()
+
+ @property
+ def get_avc_message(self):
+ """
+ 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.
+ """
+ return self._avc_queue.get()
+
+ @property
+ def get_test_results(self):
+ """
+ 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.
+ """
+ return self._test_results_queue.get()
+
+ def _start(self):
+ """
+ Start the OMCI Communications Channel
+ """
+ assert self._enabled, 'Start should only be called if enabled'
+ #
+ # TODO: Perform any other common startup tasks here
+ #
+ self.flush()
+
+ device = self._adapter_agent.get_device(self._device_id)
+ self._proxy_address = device.proxy_address
+
+ def _stop(self):
+ """
+ Stop the OMCI Communications Channel
+ """
+ assert not self._enabled, 'Stop should only be called if disabled'
+ #
+ # TODO: Perform common shutdown tasks here
+ #
+ self.flush()
+ self._proxy_address = None
+
+ # TODO: What is best way to clean up any outstanding futures for these queues
+ self._alarm_queue = None
+ self._avc_queue = None
+ self._test_results_queue = None
+
+ def _receive_onu_message(self, rx_frame):
+ """ Autonomously generated ONU frame Rx handler"""
+ # TODO: Best way to handle autonomously generated ONU frames may be pub/sub method
+ from twisted.internet.defer import QueueOverflow
+ self.log.debug('rx-onu-frame', frame_type=type(rx_frame),
+ frame=hexify(str(rx_frame)))
+
+ # TODO: Signal, via defer if Alarm Overflow or just an event?
+ msg_type = rx_frame.fields['message_type']
+
+ self._rx_onu_frames += 1
+
+ if msg_type == EntityOperations.AlarmNotification.value:
+ try:
+ self._alarm_queue.put((rx_frame, arrow.utcnow().float_timestamp))
+
+ except QueueOverflow:
+ self._rx_alarm_overflow += 1
+ self.log.warn('onu-rx-alarm-overflow', cnt=self._rx_alarm_overflow)
+
+ elif msg_type == EntityOperations.AttributeValueChange.value:
+ try:
+ self._alarm_queue.put((rx_frame, arrow.utcnow().float_timestamp))
+
+ except QueueOverflow:
+ self._rx_avc_overflow += 1
+ self.log.warn('onu-rx-avc-overflow', cnt=self._rx_avc_overflow)
+ else:
+ # TODO: Need to add test results message support
+
+ self.log.warn('onu-unsupported-autonomous-message', type=msg_type)
+ self._rx_onu_discards += 1
+
+ def receive_message(self, msg):
+ """
+ Receive and OMCI message from the proxy channel to the OLT.
+
+ Call this from your ONU Adapter on a new OMCI Rx on the proxy channel
+ """
+ if self.enabled:
+ try:
+ now = arrow.utcnow()
+ d = None
+
+ # NOTE: Since we may need to do an independent ME map on a per-ONU basis
+ # save the current value of the entity_id_to_class_map, then
+ # replace it with our custom one before decode, and then finally
+ # restore it later. Tried other ways but really made the code messy.
+
+ saved_me_map = omci_entities.entity_id_to_class_map
+ omci_entities.entity_id_to_class_map = self._me_map
+
+ try:
+ rx_frame = OmciFrame(msg)
+ rx_tid = rx_frame.fields['transaction_id']
+
+ if rx_tid == 0:
+ return self._receive_onu_message(rx_frame)
+
+ self._rx_frames += 1
+ self._consecutive_errors = 0
+
+ except KeyError as e:
+ # 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
+
+ except Exception as e:
+ self.log.exception('frame-decode', msg=hexlify(msg), e=e)
+ return
+
+ finally:
+ omci_entities.entity_id_to_class_map = saved_me_map # Always restore it.
+
+ try:
+ (ts, d, _, _) = self._requests.pop(rx_tid)
+
+ ts_diff = now - arrow.Arrow.utcfromtimestamp(ts)
+ secs = ts_diff.total_seconds()
+ self._reply_sum += secs
+
+ if secs < self._reply_min:
+ self._reply_min = secs
+
+ if secs > self._reply_max:
+ self._reply_max = secs
+
+ # TODO: Could also validate response type based on request action
+
+ except KeyError as e:
+ # Possible late Rx on a message that timed-out
+ self._rx_unknown_tid += 1
+ self.log.warn('tx-message-missing', rx_id=rx_tid, msg=hexlify(msg))
+ return
+
+ except Exception as e:
+ self.log.exception('frame-match', msg=hexlify(msg), e=e)
+ if d is not None:
+ return d.errback(failure.Failure(e))
+ return
+
+ d.callback(rx_frame)
+
+ except Exception as e:
+ self.log.exception('rx-msg', e=e)
+
+ def flush(self, max_age=0):
+ limit = arrow.utcnow().float_timestamp - max_age
+ old = [tid for tid, (ts, _, _, _) in self._requests.iteritems()
+ if ts <= limit]
+
+ for tid in old:
+ (_, d, _, _) = self._requests.pop(tid)
+ if d is not None and not d.called:
+ d.cancel()
+
+ self._requests = dict()
+
+ if max_age == 0:
+ # Flush autonomous messages (Alarms & AVCs)
+ while self._alarm_queue.pending:
+ _ = yield self._alarm_queue.get()
+
+ while self._avc_queue.pending:
+ _ = yield self._avc_queue.get()
+
+ def _get_tx_tid(self):
+ """
+ Get the next Transaction ID for a tx. Note TID=0 is reserved
+ for autonomously generated messages from an ONU
+
+ :return: (int) TID
+ """
+ tx_tid, self._tx_tid = self._tx_tid, self._tx_tid + 1
+ if self._tx_tid > MAX_OMCI_TX_ID:
+ self._tx_tid = 1
+
+ return tx_tid
+
+ def _request_failure(self, value, tx_tid):
+ """
+ Handle a transmit failure and/or Rx timeout
+
+ :param value: (Failure) Twisted failure
+ :param tx_tid: (int) Associated Tx TID
+ """
+ if tx_tid in self._requests:
+ (_, _, _, timeout) = self._requests.pop(tx_tid)
+ else:
+ timeout = 0
+
+ if isinstance(value, failure.Failure):
+ value.trap(CancelledError)
+ self._rx_timeouts += 1
+ self._consecutive_errors += 1
+ self.log.info('timeout', tx_id=tx_tid, timeout=timeout)
+ value = failure.Failure(TimeoutError(timeout, "Deferred"))
+
+ return value
+
+ def _request_success(self, rx_frame):
+ """
+ Handle transmit success (a matching Rx was received)
+
+ :param rx_frame: (OmciFrame) OMCI response frame with matching TID
+ :return: (OmciFrame) OMCI response frame with matching TID
+ """
+ #
+ # 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
+
+ elif isinstance(rx_frame.omci_message, OmciSetResponse):
+ pass # TODO: Implement MIB update
+
+ elif isinstance(rx_frame.omci_message, OmciCreateResponse):
+ pass # TODO: Implement MIB update
+
+ elif isinstance(rx_frame.omci_message, OmciDeleteResponse):
+ pass # TODO: Implement MIB update
+
+ except Exception as e:
+ self.log.exception('omci-message', e=e)
+ raise
+
+ return rx_frame
+
+ def send(self, frame, timeout=DEFAULT_OMCI_TIMEOUT):
+ """
+ Send the OMCI Frame to the ONU via the proxy_channel
+
+ :param frame: (OMCIFrame) Message to send
+ :param timeout: (int) Rx Timeout. 0=Forever
+ :return: (deferred) A deferred that fires when the response frame is received
+ or if an error/timeout occurs
+ """
+ self.flush(max_age=MAX_OMCI_REQUEST_AGE)
+
+ assert timeout <= MAX_OMCI_REQUEST_AGE, \
+ 'Maximum timeout is {} seconds'.format(MAX_OMCI_REQUEST_AGE)
+ assert isinstance(frame, OmciFrame), \
+ "Invalid frame class '{}'".format(type(frame))
+
+ if not self.enabled or self._proxy_address is None:
+ # TODO custom exceptions throughout this code would be helpful
+ return fail(result=failure.Failure(Exception('OMCI is not enabled')))
+
+ try:
+ tx_tid = frame.fields['transaction_id']
+ if tx_tid is None:
+ tx_tid = self._get_tx_tid()
+ frame.fields['transaction_id'] = tx_tid
+
+ assert tx_tid not in self._requests, 'TX TID {} is already exists'.format(tx_tid)
+ assert tx_tid >= 0, 'Invalid Tx TID: {}'.format(tx_tid)
+
+ ts = arrow.utcnow().float_timestamp
+ d = defer.Deferred()
+
+ # NOTE: Since we may need to do an independent ME map on a per-ONU basis
+ # save the current value of the entity_id_to_class_map, then
+ # replace it with our custom one before decode, and then finally
+ # restore it later. Tried other ways but really made the code messy.
+
+ saved_me_map = omci_entities.entity_id_to_class_map
+ omci_entities.entity_id_to_class_map = self._me_map
+ try:
+ self._adapter_agent.send_proxied_message(self._proxy_address,
+ hexify(str(frame)))
+ finally:
+ omci_entities.entity_id_to_class_map = saved_me_map
+
+ self._tx_frames += 1
+ self._requests[tx_tid] = (ts, d, frame, timeout)
+
+ d.addCallbacks(self._request_success, self._request_failure,
+ errbackArgs=(tx_tid,))
+
+ if timeout > 0:
+ d.addTimeout(timeout, reactor)
+
+ except Exception as e:
+ self._tx_errors += 1
+ self._consecutive_errors += 1
+ self.log.exception('send-omci', e=e)
+ return fail(result=failure.Failure(e))
+
+ return d
+
+ ###################################################################################
+ # MIB Action shortcuts
+
+ def send_mib_reset(self, timeout=DEFAULT_OMCI_TIMEOUT):
+ """
+ Perform a MIB Reset
+ """
+ self.log.debug('send-mib-reset')
+
+ 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 = 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 = 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/extensions/omci/omci_defs.py b/voltha/extensions/omci/omci_defs.py
index 4acf97d..5bd6841 100644
--- a/voltha/extensions/omci/omci_defs.py
+++ b/voltha/extensions/omci/omci_defs.py
@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-from enum import Enum
+from enum import Enum, IntEnum
from scapy.fields import PadField
from scapy.packet import Raw
@@ -102,3 +102,16 @@
TestResult = 27
GetCurrentData = 28
SetTable = 29 # Defined in Extended Message Set Only
+
+
+class ReasonCodes(IntEnum):
+ # OMCI Result and reason codes
+ Success = 0, # Command processed successfully
+ ProcessingError = 1, # Command processing error
+ NotSupported = 2, # Command not supported
+ ParameterError = 3, # Parameter error
+ UnknownEntity = 4, # Unknown managed entity
+ UnknownInstance = 5, # Unknown managed entity instance
+ DeviceBusy = 6, # Device busy
+ InstanceExists = 7, # Instance Exists
+ AttributeFailure = 9, # Attribute(s) failed or unknown
diff --git a/voltha/extensions/omci/omci_me.py b/voltha/extensions/omci/omci_me.py
new file mode 100644
index 0000000..427c1e7
--- /dev/null
+++ b/voltha/extensions/omci/omci_me.py
@@ -0,0 +1,664 @@
+#
+# 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.
+#
+"""
+OMCI Managed Entity Frame support
+"""
+from voltha.extensions.omci.omci import *
+from voltha.extensions.omci.me_frame import MEFrame
+
+
+class CardholderFrame(MEFrame):
+ """
+ This managed entity represents fixed equipment slot configuration
+ for the ONU
+ """
+ def __init__(self, single, slot_number, attributes):
+ """
+ :param single:(bool) True if the ONU is a single piece of integrated equipment,
+ False if the ONU contains pluggable equipment modules
+ :param slot_number: (int) slot number (0..254)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ # Validate
+ MEFrame.check_type(single, bool)
+ MEFrame.check_type(slot_number, int)
+ if not 0 <= slot_number <= 254:
+ raise ValueError('slot_number should be 0..254')
+
+ entity_id = 256 + slot_number if single else slot_number
+
+ super(CardholderFrame, self).__init__(Cardholder, entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class CircuitPackFrame(MEFrame):
+ """
+ This managed entity models a real or virtual circuit pack that is equipped in
+ a real or virtual ONU slot.
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Its value is the same as that
+ of the cardholder managed entity containing this
+ circuit pack instance. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(CircuitPackFrame, self).__init__(CircuitPack, entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class ExtendedVlanTaggingOperationConfigurationDataFrame(MEFrame):
+ """
+ This managed entity organizes data associated with VLAN tagging. Regardless
+ of its point of attachment, the specified tagging operations refer to the
+ upstream direction.
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. Its value is the same as that
+ of the cardholder managed entity containing this
+ circuit pack instance. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(ExtendedVlanTaggingOperationConfigurationDataFrame,
+ self).__init__(ExtendedVlanTaggingOperationConfigurationData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class IpHostConfigDataFrame(MEFrame):
+ """
+ The IP host config data configures IPv4 based services offered on the ONU.
+ """
+ def __init__(self, entity_id, attributes):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(IpHostConfigDataFrame, self).__init__(IpHostConfigData,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class GalEthernetProfileFrame(MEFrame):
+ """
+ This managed entity organizes data that describe the GTC adaptation layer
+ processing functions of the ONU for Ethernet services.
+ """
+ def __init__(self, entity_id, max_gem_payload_size=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param max_gem_payload_size: (int) This attribute defines the maximum payload
+ size generated in the associated GEM interworking
+ termination point managed entity. (0..65535
+ """
+ MEFrame.check_type(max_gem_payload_size, (int, type(None)))
+ if max_gem_payload_size is not None and not 0 <= max_gem_payload_size <= 0xFFFF: # TODO: verify min/max
+ raise ValueError('max_gem_payload_size should be 0..0xFFFF')
+
+ data = None if max_gem_payload_size is None else\
+ {
+ 'max_gem_payload_size': max_gem_payload_size
+ }
+ super(GalEthernetProfileFrame, self).__init__(GalEthernetProfile,
+ entity_id,
+ data)
+
+
+class GemInterworkingTpFrame(MEFrame):
+ """
+ An instance of this managed entity represents a point in the ONU where the
+ interworking of a bearer service (usually Ethernet) to the GEM layer takes
+ place.
+ """
+ def __init__(self, entity_id,
+ gem_port_network_ctp_pointer=None,
+ interworking_option=None,
+ service_profile_pointer=None,
+ interworking_tp_pointer=None,
+ pptp_counter=None,
+ gal_profile_pointer=None,
+ attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param gem_port_network_ctp_pointer: (int) This attribute points to an instance of
+ the GEM port network CTP. (0..65535)
+
+ :param interworking_option: (int) This attribute identifies the type
+ of non-GEM function that is being interworked.
+ The options are:
+ 0 Circuit-emulated TDM
+ 1 MAC bridged LAN
+ 2 Reserved
+ 3 Reserved
+ 4 Video return path
+ 5 IEEE 802.1p mapper
+ 6 Downstream broadcast
+ 7 MPLS PW TDM service
+
+ :param service_profile_pointer: (int) This attribute points to an instance of
+ a service profile.
+ CES service profile if interworking option = 0
+ MAC bridge service profile if interworking option = 1
+ Video return path service profile if interworking option = 4
+ IEEE 802.1p mapper service profile if interworking option = 5
+ Null pointer if interworking option = 6
+ CES service profile if interworking option = 7
+
+ :param interworking_tp_pointer: (int) This attribute is used for the circuit
+ emulation service and IEEE 802.1p mapper
+ service without a MAC bridge.
+
+ :param gal_profile_pointer: (int) This attribute points to an instance of
+ a service profile.
+
+ :param attributes: (basestring, list, set, dict) additional ME attributes.
+ not specifically specified as a parameter. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified..
+ """
+ # Validate
+ self.check_type(gem_port_network_ctp_pointer, (int, type(None)))
+ self.check_type(interworking_option, (int, type(None)))
+ self.check_type(service_profile_pointer, (int, type(None)))
+ self.check_type(interworking_tp_pointer,(int, type(None)))
+ self.check_type(pptp_counter,(int, type(None)))
+ self.check_type(gal_profile_pointer, (int, type(None)))
+
+ if gem_port_network_ctp_pointer is not None and not 0 <= gem_port_network_ctp_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('gem_port_network_ctp_pointer should be 0..0xFFFE')
+
+ if interworking_option is not None and not 0 <= interworking_option <= 7:
+ raise ValueError('interworking_option should be 0..7')
+
+ if service_profile_pointer is not None and not 0 <= service_profile_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('service_profile_pointer should be 0..0xFFFE')
+
+ if interworking_tp_pointer is not None and not 0 <= interworking_tp_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('interworking_tp_pointer should be 0..0xFFFE')
+
+ if pptp_counter is not None and not 0 <= pptp_counter <= 255: # TODO: Verify max
+ raise ValueError('pptp_counter should be 0..255')
+
+ if gal_profile_pointer is not None and not 0 <= gal_profile_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('gal_profile_pointer should be 0..0xFFFE')
+
+ data = MEFrame._attr_to_data(attributes)
+
+ if gem_port_network_ctp_pointer is not None or \
+ interworking_option is not None or \
+ service_profile_pointer is not None or \
+ interworking_tp_pointer is not None or \
+ gal_profile_pointer is not None:
+
+ data = data or dict()
+
+ if gem_port_network_ctp_pointer is not None:
+ data['gem_port_network_ctp_pointer'] = gem_port_network_ctp_pointer
+
+ if interworking_option is not None:
+ data['interworking_option'] = interworking_option
+
+ if service_profile_pointer is not None:
+ data['service_profile_pointer'] = service_profile_pointer
+
+ if interworking_tp_pointer is not None:
+ data['interworking_tp_pointer'] = interworking_tp_pointer
+
+ if gal_profile_pointer is not None:
+ data['gal_profile_pointer'] = gal_profile_pointer
+
+ super(GemInterworkingTpFrame, self).__init__(GemInterworkingTp,
+ entity_id,
+ data)
+
+
+class GemPortNetworkCtpFrame(MEFrame):
+ """
+ This managed entity represents the termination of a GEM port on an ONU.
+ """
+ def __init__(self, entity_id, port_id=None, tcont_id=None,
+ direction=None, upstream_tm=None, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param port_id: (int) This attribute is the port-ID of the GEM port associated
+ with this CTP
+
+ :param tcont_id: (int) This attribute points to a T-CONT instance
+
+ :param direction: (string) Data direction. Valid values are:
+ 'upstream' - UNI-to-ANI
+ 'downstream' - ANI-to-UNI
+ 'bi-directional' - guess :-)
+
+ :param upstream_tm: (int) If the traffic management option attribute in
+ the ONU-G ME is 0 (priority controlled) or 2
+ (priority and rate controlled), this pointer
+ specifies the priority queue ME serving this GEM
+ port network CTP. If the traffic management
+ option attribute is 1 (rate controlled), this
+ attribute redundantly points to the T-CONT serving
+ this GEM port network CTP.
+
+ :param attributes: (basestring, list, set, dict) additional ME attributes.
+ not specifically specified as a parameter. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ _directions = {"upstream": 1, "downstream": 2, "bi-directional": 3}
+
+ # Validate
+ self.check_type(port_id, (int, type(None)))
+ self.check_type(tcont_id, (int, type(None)))
+ self.check_type(direction, (basestring, type(None)))
+ self.check_type(upstream_tm, (int, type(None)))
+
+ if port_id is not None and not 0 <= port_id <= 0xFFFE: # TODO: Verify max
+ raise ValueError('port_id should be 0..0xFFFE')
+
+ if tcont_id is not None and not 0 <= tcont_id <= 0xFFFE: # TODO: Verify max
+ raise ValueError('tcont_id should be 0..0xFFFE')
+
+ if direction is not None and str(direction).lower() not in _directions:
+ raise ValueError('direction should one of {}'.format(_directions.keys()))
+
+ if upstream_tm is not None and not 0 <= upstream_tm <= 0xFFFE: # TODO: Verify max
+ raise ValueError('upstream_tm should be 0..0xFFFE')
+
+ data = MEFrame._attr_to_data(attributes)
+
+ if port_id is not None or tcont_id is not None or\
+ direction is not None or upstream_tm is not None:
+
+ data = data or dict()
+
+ if port_id is not None:
+ data['port_id'] = port_id
+ if tcont_id is not None:
+ data['tcont_pointer'] = tcont_id
+ if direction is not None:
+ data['direction'] = _directions[str(direction).lower()]
+ if upstream_tm is not None:
+ data['traffic_management_pointer_upstream'] = upstream_tm
+
+ super(GemPortNetworkCtpFrame, self).__init__(GemPortNetworkCtp,
+ entity_id,
+ data)
+
+
+class Ieee8021pMapperServiceProfileFrame(MEFrame):
+ """
+ This managed entity associates the priorities of IEEE 802.1p [IEEE
+ 802.1D] priority tagged frames with specific connections.
+ """
+ def __init__(self, entity_id, tp_pointer=None, interwork_tp_pointers=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param tp_pointer: (int) This attribute points to an instance of the
+ associated termination point. (0..65535)
+
+ :param interwork_tp_pointers: (list) List of 1 to 8 interworking termination
+ point IDs. The first entry is assigned
+ got p-bit priority 0. If less than 8 IDs
+ are provided, the last ID is used for
+ the remaining items.
+ """
+ if tp_pointer is None and interwork_tp_pointers is None:
+ data = dict(
+ tp_pointer=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_0=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_1=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_2=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_3=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_4=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_5=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_6=OmciNullPointer,
+ interwork_tp_pointer_for_p_bit_priority_7=OmciNullPointer
+ )
+ else:
+ self.check_type(tp_pointer, (list, type(None)))
+ self.check_type(interwork_tp_pointers, (list, type(None)))
+
+ data = dict()
+
+ if tp_pointer is not None:
+ data['tp_pointer'] = tp_pointer
+
+ if interwork_tp_pointers is not None:
+ assert all(isinstance(tp, int) and 0 <= tp <= 0xFFFF
+ for tp in interwork_tp_pointers),\
+ 'Interworking TP IDs must be 0..0xFFFF'
+ assert 1 <= len(interwork_tp_pointers) <= 8, \
+ 'Invalid number of Interworking TP IDs. Must be 1..8'
+
+ data = dict()
+ for pbit in range(0, len(interwork_tp_pointers)):
+ data['interwork_tp_pointer_for_p_bit_priority_{}'.format(pbit)] = \
+ interwork_tp_pointers[pbit]
+
+ for pbit in range(len(interwork_tp_pointers), 8):
+ data['interwork_tp_pointer_for_p_bit_priority_{}'.format(pbit)] = \
+ interwork_tp_pointers[len(interwork_tp_pointers) - 1]
+
+ super(Ieee8021pMapperServiceProfileFrame, self).__init__(Ieee8021pMapperServiceProfile,
+ entity_id,
+ data)
+
+
+class MacBridgePortConfigurationDataFrame(MEFrame):
+ """
+ This managed entity represents the ONU as equipment.
+ """
+ def __init__(self, entity_id, bridge_id_pointer=None, port_num=None,
+ tp_type=None, tp_pointer=None, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param bridge_id_pointer: (int) This attribute points to an instance of the
+ MAC bridge service profile. (0..65535)
+
+ :param port_num: (int) This attribute is the bridge port number. (0..255)
+
+ :param tp_type: (int) This attribute identifies the type of termination point
+ associated with this MAC bridge port. Valid values are:
+ 1 Physical path termination point Ethernet UNI
+ 2 Interworking VCC termination point
+ 3 IEEE 802.1p mapper service profile
+ 4 IP host config data or IPv6 host config data
+ 5 GEM interworking termination point
+ 6 Multicast GEM interworking termination point
+ 7 Physical path termination point xDSL UNI part 1
+ 8 Physical path termination point VDSL UNI
+ 9 Ethernet flow termination point
+ 10 Reserved
+ 11 Virtual Ethernet interface point
+ 12 Physical path termination point MoCA UNI
+
+ :param tp_pointer: (int) This attribute points to the termination point
+ associated with this MAC bridge por. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) additional ME attributes.
+ not specifically specified as a parameter. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ # Validate
+ self.check_type(bridge_id_pointer, (int, type(None)))
+ self.check_type(port_num, (int, type(None)))
+ self.check_type(tp_type, (int, type(None)))
+ self.check_type(tp_pointer, (int, type(None)))
+
+ if bridge_id_pointer is not None and not 0 <= bridge_id_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('bridge_id_pointer should be 0..0xFFFE')
+
+ if port_num is not None and not 0 <= port_num <= 255:
+ raise ValueError('port_num should be 0..255') # TODO: Verify min,max
+
+ if tp_type is not None and not 1 <= tp_type <= 12:
+ raise ValueError('service_profile_pointer should be 1..12')
+
+ if tp_pointer is not None and not 0 <= tp_pointer <= 0xFFFE: # TODO: Verify max
+ raise ValueError('interworking_tp_pointer should be 0..0xFFFE')
+
+ data = MEFrame._attr_to_data(attributes)
+
+ if bridge_id_pointer is not None or \
+ port_num is not None or \
+ tp_type is not None or \
+ tp_pointer is not None:
+
+ data = data or dict()
+
+ if bridge_id_pointer is not None:
+ data['bridge_id_pointer'] = bridge_id_pointer
+
+ if port_num is not None:
+ data['port_num'] = port_num
+
+ if tp_type is not None:
+ data['tp_type'] = tp_type
+
+ if tp_pointer is not None:
+ data['tp_pointer'] = tp_pointer
+
+ super(MacBridgePortConfigurationDataFrame, self).\
+ __init__(MacBridgePortConfigurationData, entity_id, data)
+
+
+class MacBridgeServiceProfileFrame(MEFrame):
+ """
+ This managed entity models a MAC bridge in its entirety; any number
+ of ports may be associated with the bridge through pointers to the
+ MAC bridge service profile managed entity.
+ """
+ def __init__(self, entity_id, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(MacBridgeServiceProfileFrame, self).__init__(MacBridgeServiceProfile,
+ entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class OntGFrame(MEFrame):
+ """
+ This managed entity represents the ONU as equipment.
+ """
+ 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
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(OntGFrame, self).__init__(OntG, 0,
+ MEFrame._attr_to_data(attributes))
+
+
+class Ont2GFrame(MEFrame):
+ """
+ This managed entity contains additional attributes associated with a PON ONU.
+ """
+ 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
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ # Only one managed entity instance (Entity ID=0)
+ super(Ont2GFrame, self).__init__(Ont2G, 0,
+ MEFrame._attr_to_data(attributes))
+
+
+class PptpEthernetUniFrame(MEFrame):
+ """
+ This managed entity represents the point at an Ethernet UNI where the physical path
+ terminates and Ethernet physical level functions are performed.
+ """
+ def __init__(self, entity_id, attributes=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param attributes: (basestring, list, set, dict) attributes. For gets
+ a string, list, or set can be provided. For create/set
+ operations, a dictionary should be provided, for
+ deletes None may be specified.
+ """
+ super(PptpEthernetUniFrame, self).__init__(PptpEthernetUni, entity_id,
+ MEFrame._attr_to_data(attributes))
+
+
+class SoftwareImageFrame(MEFrame):
+ """
+ This managed entity models an executable software image stored in the ONU.
+ """
+ def __init__(self, entity_id):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+ """
+ super(SoftwareImageFrame, self).__init__(SoftwareImage, entity_id, None)
+
+
+class TcontFrame(MEFrame):
+ """
+ An instance of the traffic container managed entity T-CONT represents a
+ logical connection group associated with a G-PON PLOAM layer alloc-ID.
+ """
+ def __init__(self, entity_id, alloc_id=None, policy=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param alloc_id: (int) This attribute links the T-CONT with the alloc-ID
+ assigned by the OLT in the assign_alloc-ID PLOAM
+ message (0..0xFFF)
+
+ :param policy: (int) This attribute indicates the T-CONT's traffic scheduling
+ policy. Valid values:
+ 0 - Null
+ 1 - Strict priority
+ 2 - WRR - Weighted round robin
+ """
+ # Validate
+ self.check_type(alloc_id, (int, type(None)))
+ self.check_type(policy, (int, type(None)))
+
+ if alloc_id is not None and not 0 <= alloc_id <= 0xFFF:
+ raise ValueError('alloc_id should be 0..0xFFF')
+
+ if policy is not None and not 0 <= policy <= 2:
+ raise ValueError('policy should be 0..2')
+
+ if alloc_id is None and policy is None:
+ data = None
+ else:
+ data = dict()
+
+ if alloc_id is not None:
+ data['alloc_id'] = alloc_id
+
+ if policy is not None:
+ data['policy'] = policy
+
+ super(TcontFrame, self).__init__(Tcont, entity_id, data)
+
+
+class VlanTaggingFilterDataFrame(MEFrame):
+ """
+ An instance of this managed entity represents a point in the ONU where the
+ interworking of a bearer service (usually Ethernet) to the GEM layer takes
+ place.
+ """
+ def __init__(self, entity_id, vlan_tcis=None, forward_operation=None):
+ """
+ :param entity_id: (int) This attribute uniquely identifies each instance of
+ this managed entity. (0..65535)
+
+ :param vlan_tcis: (list) This attribute is a list of provisioned TCI values
+ for the bridge port. (0..0xFFFF)
+
+ :param forward_operation: (int) What to do. See ITU spec for more information
+
+ """
+ # Validate
+ self.check_type(vlan_tcis, (list, type(None)))
+ self.check_type(forward_operation, (int, type(None)))
+
+ if forward_operation is not None and not 0 <= forward_operation <= 0x21:
+ raise ValueError('forward_operation should be 0..0x21')
+
+ if vlan_tcis is None and forward_operation is None:
+ data = None
+
+ else:
+ data = dict()
+
+ if vlan_tcis is not None:
+ assert all(isinstance(tci, int) and 0 <= tci <= 0xFFFF
+ for tci in vlan_tcis), "VLAN TCI's are 0..0xFFFF"
+ assert 1 <= len(vlan_tcis) <= 12, 'Number of VLAN TCI values is 1..12'
+
+ for index in range(0, len(vlan_tcis)):
+ data['vlan_filter_{}'.format(index)] = vlan_tcis[index]
+
+ data['number_of_entries'] = len(vlan_tcis)
+
+ if forward_operation is not None:
+ assert 0 <= forward_operation <= 0x21, 'forwarding_operation must be 0x00..0x21'
+ data['forward_operation'] = forward_operation
+
+ 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/extensions/omci/omci_messages.py b/voltha/extensions/omci/omci_messages.py
index 65e291a..2dee1e3 100644
--- a/voltha/extensions/omci/omci_messages.py
+++ b/voltha/extensions/omci/omci_messages.py
@@ -14,13 +14,12 @@
# limitations under the License.
#
import structlog
-from enum import Enum
from scapy.fields import ByteField, StrFixedLenField, ConditionalField, Field
from scapy.fields import ShortField, BitField
from scapy.packet import Packet
from voltha.extensions.omci.omci_defs import AttributeAccess
-from voltha.extensions.omci.omci_entities import entity_id_to_class_map
+import voltha.extensions.omci.omci_entities as omci_entities
log = structlog.get_logger()
@@ -36,27 +35,27 @@
def addfield(self, pkt, s, val):
class_id = getattr(pkt, self._entity_class)
- entity_class = entity_id_to_class_map.get(class_id)
+ entity_class = omci_entities.entity_id_to_class_map.get(class_id)
for attribute in entity_class.attributes:
- if AttributeAccess.SetByCreate not in attribute._access:
+ if AttributeAccess.SetByCreate not in attribute.access:
continue
- if attribute._fld.name == 'managed_entity_id':
+ if attribute.field.name == 'managed_entity_id':
continue
- fld = attribute._fld
+ fld = attribute.field
s = fld.addfield(pkt, s, val.get(fld.name, fld.default))
return s
def getfield(self, pkt, s):
"""Extract an internal value from a string"""
class_id = getattr(pkt, self._entity_class)
- entity_class = entity_id_to_class_map.get(class_id)
+ entity_class = omci_entities.entity_id_to_class_map.get(class_id)
data = {}
for attribute in entity_class.attributes:
- if AttributeAccess.SetByCreate not in attribute._access:
+ if AttributeAccess.SetByCreate not in attribute.access:
continue
- if attribute._fld.name == 'managed_entity_id':
+ if attribute.field.name == 'managed_entity_id':
continue
- fld = attribute._fld
+ fld = attribute.field
s, value = fld.getfield(pkt, s)
data[fld.name] = value
return s, data
@@ -75,10 +74,10 @@
def addfield(self, pkt, s, val):
class_id = getattr(pkt, self._entity_class)
attribute_mask = getattr(pkt, self._attributes_mask)
- entity_class = entity_id_to_class_map.get(class_id)
+ entity_class = omci_entities.entity_id_to_class_map.get(class_id)
indices = entity_class.attribute_indices_from_mask(attribute_mask)
for index in indices:
- fld = entity_class.attributes[index]._fld
+ fld = entity_class.attributes[index].field
s = fld.addfield(pkt, s, val[fld.name])
return s
@@ -86,22 +85,22 @@
"""Extract an internal value from a string"""
class_id = getattr(pkt, self._entity_class)
attribute_mask = getattr(pkt, self._attributes_mask)
- entity_class = entity_id_to_class_map[class_id]
+ entity_class = omci_entities.entity_id_to_class_map[class_id]
indices = entity_class.attribute_indices_from_mask(attribute_mask)
data = {}
for index in indices:
try:
- fld = entity_class.attributes[index]._fld
+ fld = entity_class.attributes[index].field
except IndexError, e:
- log.error("Cannot decode attribute {} for entity class {}".format(
- index, entity_class))
+ log.error("attribute-decode-failure", attribute_index=index,
+ entity_class=entity_class, e=e)
continue
try:
s, value = fld.getfield(pkt, s)
except Exception, e:
raise
data[fld.name] = value
- return s, data
+ return s, data
class OmciMessage(Packet):