VOL-1330: Update openomci to voltha 1.x master.
Includes mib resync/reconcile fixes:
Author: Chip Boling <chip@bcsw.net>
Date: Fri Feb 22 13:06:25 2019 -0600
VOL-1482: Fix Scapy definition for OMCI GetResponse message
Original-Change-Id: I155ff3f5914b81f9a09aede97c2a7cafc1b088fe
Author: Chip Boling <chip@bcsw.net>
Date: Mon Mar 4 13:33:22 2019 -0600
VOL-1504: fix for TimeSynchronization Request frame
Original-Change-Id: I5350b765506ef9d19639c54281d38911a6f4c323
Author: Chip Boling <chip@bcsw.net>
Date: Wed Feb 27 12:44:07 2019 -0600
VOL-1439: Fixes for proper table attribute handling
during MIB audit/resynchronization. Also includes a fix to
properly count MIB-DATA-SYNC increments on sets and software-download
operations
Original-Change-Id: I30a343aae91d5bcac56d068a37c18b29265d3bd9
Change-Id: If30bd6ea0fd59db5dbf51ecd617d000baf538728
diff --git a/VERSION b/VERSION
index 7e541ae..2d2a95e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.2.2
\ No newline at end of file
+2.2.3-dev0
diff --git a/pyvoltha/adapters/extensions/omci/database/mib_db_ext.py b/pyvoltha/adapters/extensions/omci/database/mib_db_ext.py
index 7523f58..7120dde 100644
--- a/pyvoltha/adapters/extensions/omci/database/mib_db_ext.py
+++ b/pyvoltha/adapters/extensions/omci/database/mib_db_ext.py
@@ -548,7 +548,7 @@
try:
data = MibDeviceData()
- path = get_device_path(device_id)
+ path = self._get_device_path(device_id)
data.ParseFromString(self._kv_store[path])
return int(data.mib_data_sync)
diff --git a/pyvoltha/adapters/extensions/omci/me_frame.py b/pyvoltha/adapters/extensions/omci/me_frame.py
index 06ae2bb..71e342f 100644
--- a/pyvoltha/adapters/extensions/omci/me_frame.py
+++ b/pyvoltha/adapters/extensions/omci/me_frame.py
@@ -339,6 +339,7 @@
entity_id=getattr(self, 'entity_id'),
year=dt.year,
month=dt.month,
+ day=dt.day,
hour=dt.hour,
minute=dt.minute,
second=dt.second,
diff --git a/pyvoltha/adapters/extensions/omci/omci_cc.py b/pyvoltha/adapters/extensions/omci/omci_cc.py
index 4b695a5..76aa41e 100644
--- a/pyvoltha/adapters/extensions/omci/omci_cc.py
+++ b/pyvoltha/adapters/extensions/omci/omci_cc.py
@@ -60,7 +60,12 @@
MIB_Reset = 8,
Connectivity = 9,
Get_ALARM_Get = 10,
- Get_ALARM_Get_Next = 11
+ Get_ALARM_Get_Next = 11,
+ Start_Software_Download = 12,
+ Download_Section = 13,
+ End_Software_Download = 14,
+ Activate_Software = 15,
+ Commit_Software = 15,
# abbreviations
@@ -71,8 +76,8 @@
class OMCI_CC(object):
""" Handle OMCI Communication Channel specifics for Adtran ONUs"""
- MIN_OMCI_TX_ID_LOW_PRIORITY = 0x0001 # 2 Octets max
- MAX_OMCI_TX_ID_LOW_PRIORITY = 0x7FFF # 2 Octets max
+ MIN_OMCI_TX_ID_LOW_PRIORITY = 0x0001 # 2 Octets max
+ MAX_OMCI_TX_ID_LOW_PRIORITY = 0x7FFF # 2 Octets max
MIN_OMCI_TX_ID_HIGH_PRIORITY = 0x8000 # 2 Octets max
MAX_OMCI_TX_ID_HIGH_PRIORITY = 0xFFFF # 2 Octets max
LOW_PRIORITY = 0
@@ -352,7 +357,7 @@
rx_tid = rx_frame.fields['transaction_id']
msg_type = rx_frame.fields['message_type']
- self.log.debug('Received message for rx_tid', rx_tid = rx_tid)
+ self.log.debug('Received message for rx_tid', rx_tid = rx_tid, msg_type = msg_type)
# Filter the Test Result frame and route through receive onu
# message method.
if rx_tid == 0 or msg_type == EntityOperations.TestResult.value:
diff --git a/pyvoltha/adapters/extensions/omci/omci_fields.py b/pyvoltha/adapters/extensions/omci/omci_fields.py
index 09cf465..8fc8a4c 100644
--- a/pyvoltha/adapters/extensions/omci/omci_fields.py
+++ b/pyvoltha/adapters/extensions/omci/omci_fields.py
@@ -250,4 +250,31 @@
for k, v in sorted(key_value_pairs.iteritems()):
table.append(v)
- return table
\ No newline at end of file
+ return table
+
+
+class OmciVariableLenZeroPadField(Field):
+ __slots__ = ["_pad_to", "_omci_hdr_len"]
+
+ def __init__(self, name, pad_to):
+ Field.__init__(self, name, 0, 'B')
+ self._pad_to = pad_to
+ self._omci_hdr_len = 4
+
+ def addfield(self, pkt, s, _val):
+ count = self._pad_to - self._omci_hdr_len - len(s)
+ if count < 0:
+ from scapy.error import Scapy_Exception
+ raise Scapy_Exception("%s: Already past pad_to offset" %
+ self.__class__.__name__)
+ padding = bytearray(count)
+ import struct
+ return s + struct.pack("%iB" % count, *padding)
+
+ def getfield(self, pkt, s):
+ count = len(s) - self._omci_hdr_len
+ if count < 0:
+ from scapy.error import Scapy_Exception
+ raise Scapy_Exception("%s: Already past pad_to offset" %
+ self.__class__.__name__)
+ return s[count:], s[-count:]
diff --git a/pyvoltha/adapters/extensions/omci/omci_me.py b/pyvoltha/adapters/extensions/omci/omci_me.py
index ea62c2a..4a28d27 100644
--- a/pyvoltha/adapters/extensions/omci/omci_me.py
+++ b/pyvoltha/adapters/extensions/omci/omci_me.py
@@ -78,7 +78,7 @@
of its point of attachment, the specified tagging operations refer to the
upstream direction.
"""
- def __init__(self, entity_id, attributes):
+ def __init__(self, entity_id, attributes=None):
"""
:param entity_id: (int) This attribute uniquely identifies each instance of
this managed entity. Its value is the same as that
diff --git a/pyvoltha/adapters/extensions/omci/omci_messages.py b/pyvoltha/adapters/extensions/omci/omci_messages.py
index c8526da..2b51bf8 100644
--- a/pyvoltha/adapters/extensions/omci/omci_messages.py
+++ b/pyvoltha/adapters/extensions/omci/omci_messages.py
@@ -19,7 +19,7 @@
from scapy.packet import Packet
from pyvoltha.adapters.extensions.omci.omci_defs import AttributeAccess, OmciSectionDataSize
-from pyvoltha.adapters.extensions.omci.omci_fields import OmciTableField
+from pyvoltha.adapters.extensions.omci.omci_fields import OmciTableField, OmciVariableLenZeroPadField
import pyvoltha.adapters.extensions.omci.omci_entities as omci_entities
@@ -198,15 +198,16 @@
ShortField("entity_id", 0),
ByteField("success_code", 0),
ShortField("attributes_mask", None),
- ConditionalField(
- ShortField("unsupported_attributes_mask", 0),
- lambda pkt: pkt.success_code == 9),
- ConditionalField(
- ShortField("failed_attributes_mask", 0),
- lambda pkt: pkt.success_code == 9),
- ConditionalField(
- OmciMaskedData("data"),
- lambda pkt: pkt.success_code == 0 or pkt.success_code == 9)
+ ConditionalField(OmciMaskedData("data"),
+ lambda pkt: pkt.success_code in (0, 9)),
+ ConditionalField(OmciVariableLenZeroPadField("zero_padding", 36),
+ lambda pkt: pkt.success_code == 9),
+
+ # These fields are only valid if attribute error (status == 9)
+ ConditionalField(ShortField("unsupported_attributes_mask", 0),
+ lambda pkt: pkt.success_code == 9),
+ ConditionalField(ShortField("failed_attributes_mask", 0),
+ lambda pkt: pkt.success_code == 9)
]
@@ -431,6 +432,7 @@
OmciMaskedData("data"), lambda pkt: pkt.success_code == 0)
]
+
class OmciStartSoftwareDownload(OmciMessage):
name = "OmciStartSoftwareDownload"
message_id = 0x53
@@ -443,6 +445,7 @@
ShortField("instance_id", None) # should be same as "entity_id"
]
+
class OmciStartSoftwareDownloadResponse(OmciMessage):
name = "OmciStartSoftwareDownloadResponse"
message_id = 0x33
@@ -455,6 +458,7 @@
ShortField("instance_id", None) # should be same as "entity_id"
]
+
class OmciEndSoftwareDownload(OmciMessage):
name = "OmciEndSoftwareDownload"
message_id = 0x55
@@ -467,6 +471,7 @@
ShortField("instance_id", None),# should be same as "entity_id"
]
+
class OmciEndSoftwareDownloadResponse(OmciMessage):
name = "OmciEndSoftwareDownload"
message_id = 0x35
@@ -479,6 +484,7 @@
ByteField("result0", 0) # same as result
]
+
class OmciDownloadSection(OmciMessage):
name = "OmciDownloadSection"
message_id = 0x14
@@ -489,6 +495,7 @@
StrFixedLenField("data", 0, length=OmciSectionDataSize) # section data
]
+
class OmciDownloadSectionLast(OmciMessage):
name = "OmciDownloadSection"
message_id = 0x54
@@ -499,6 +506,7 @@
StrFixedLenField("data", 0, length=OmciSectionDataSize) # section data
]
+
class OmciDownloadSectionResponse(OmciMessage):
name = "OmciDownloadSectionResponse"
message_id = 0x34
@@ -509,6 +517,7 @@
ByteField("section_number", 0), # Always only 1 in parallel
]
+
class OmciActivateImage(OmciMessage):
name = "OmciActivateImage"
message_id = 0x56
@@ -518,6 +527,7 @@
ByteField("activate_flag", 0) # Activate image unconditionally
]
+
class OmciActivateImageResponse(OmciMessage):
name = "OmciActivateImageResponse"
message_id = 0x36
@@ -527,6 +537,7 @@
ByteField("result", 0) # Activate image unconditionally
]
+
class OmciCommitImage(OmciMessage):
name = "OmciCommitImage"
message_id = 0x57
@@ -535,6 +546,7 @@
ShortField("entity_id", None),
]
+
class OmciCommitImageResponse(OmciMessage):
name = "OmciCommitImageResponse"
message_id = 0x37
diff --git a/pyvoltha/adapters/extensions/omci/openomci_agent.py b/pyvoltha/adapters/extensions/omci/openomci_agent.py
index 672af5b..e416d58 100644
--- a/pyvoltha/adapters/extensions/omci/openomci_agent.py
+++ b/pyvoltha/adapters/extensions/omci/openomci_agent.py
@@ -41,7 +41,7 @@
'mib-synchronizer': {
'state-machine': MibSynchronizer, # Implements the MIB synchronization state machine
'database': MibDbVolatileDict, # Implements volatile ME MIB database
- #'database': MibDbExternal, # Implements persistent ME MIB database
+ # 'database': MibDbExternal, # Implements persistent ME MIB database
'advertise-events': True, # Advertise events on OpenOMCI event bus
'tasks': {
'mib-upload': MibUploadTask,
@@ -55,7 +55,7 @@
'state-machine': OnuOmciCapabilities, # Implements OMCI capabilities state machine
'advertise-events': False, # Advertise events on OpenOMCI event bus
'tasks': {
- 'get-capabilities': OnuCapabilitiesTask # Get supported ME and Commands
+ 'get-capabilities': OnuCapabilitiesTask # Get supported ME and Commands
}
},
'performance-intervals': {
diff --git a/pyvoltha/adapters/extensions/omci/state_machines/mib_sync.py b/pyvoltha/adapters/extensions/omci/state_machines/mib_sync.py
index 3f18aa4..a5b0e2a 100644
--- a/pyvoltha/adapters/extensions/omci/state_machines/mib_sync.py
+++ b/pyvoltha/adapters/extensions/omci/state_machines/mib_sync.py
@@ -143,6 +143,10 @@
RxEvent.Create: None,
RxEvent.Delete: None,
RxEvent.Set: None,
+ RxEvent.Start_Software_Download: None,
+ RxEvent.End_Software_Download: None,
+ RxEvent.Activate_Software: None,
+ RxEvent.Commit_Software: None,
}
self._omci_cc_sub_mapping = {
RxEvent.MIB_Reset: self.on_mib_reset_response,
@@ -152,6 +156,10 @@
RxEvent.Create: self.on_create_response,
RxEvent.Delete: self.on_delete_response,
RxEvent.Set: self.on_set_response,
+ RxEvent.Start_Software_Download: self.on_software_event,
+ RxEvent.End_Software_Download: self.on_software_event,
+ RxEvent.Activate_Software: self.on_software_event,
+ RxEvent.Commit_Software: self.on_software_event,
}
self._onu_dev_subscriptions = { # DevEvent.enum -> Subscription Object
DevEvent.OmciCapabilitiesEvent: None
@@ -216,6 +224,7 @@
if self._database is not None:
self._database.save_mib_data_sync(self._device_id,
self._mib_data_sync)
+ self.log.info("mds-updated", device=self._device_id, mds=self._mib_data_sync)
@property
def last_mib_db_sync(self):
@@ -522,8 +531,9 @@
self._attr_diffs = attr_diffs if attr_diffs and len(attr_diffs) else None
self._audited_olt_db = olt_db
self._audited_onu_db = onu_db
+ audited_mds = self._audited_onu_db[MDS_KEY]
- mds_equal = self.mib_data_sync == self._audited_onu_db[MDS_KEY]
+ mds_equal = self.mib_data_sync == audited_mds
if mds_equal and all(diff is None for diff in [self._on_olt_only_diffs,
self._on_onu_only_diffs,
@@ -609,14 +619,6 @@
# Save the changed data to the MIB.
self._database.set(self.device_id, class_id, instance_id, data)
- # Autonomous creation and deletion of managed entities do not
- # result in an increment of the MIB data sync value. However,
- # AVC's in response to a change by the Operator do incur an
- # increment of the MIB Data Sync. If here during uploading,
- # we issued a MIB-Reset which may generate AVC. (TODO: Focus testing during hardening)
- if self.state == 'uploading':
- self.increment_mib_data_sync()
-
except KeyError:
pass # NOP
@@ -711,17 +713,16 @@
status=omci_msg.fields['success_code'],
status_text=self._status_to_text(omci_msg.fields['success_code']),
parameter_error_attributes_mask=omci_msg.fields['parameter_error_attributes_mask'])
- else:
+
+ elif status != RC.InstanceExists:
omci_msg = request.fields['omci_message'].fields
class_id = omci_msg['entity_class']
entity_id = omci_msg['entity_id']
attributes = {k: v for k, v in omci_msg['data'].items()}
# Save to the database
- created = self._database.set(self._device_id, class_id, entity_id, attributes)
-
- if created:
- self.increment_mib_data_sync()
+ self._database.set(self._device_id, class_id, entity_id, attributes)
+ self.increment_mib_data_sync()
# If the ME contains set-by-create or writeable values that were
# not specified in the create command, the ONU will have
@@ -803,10 +804,8 @@
entity_id = omci_msg['entity_id']
# Remove from the database
- deleted = self._database.delete(self._device_id, class_id, entity_id)
-
- if deleted:
- self.increment_mib_data_sync()
+ self._database.delete(self._device_id, class_id, entity_id)
+ self.increment_mib_data_sync()
except KeyError as e:
pass # NOP
@@ -825,36 +824,85 @@
if self._omci_cc_subscriptions[RxEvent.Set]:
if self.state in ['disabled', 'uploading']:
self.log.error('rx-in-invalid-state', state=self.state)
+ return
try:
request = msg[TX_REQUEST_KEY]
response = msg[RX_RESPONSE_KEY]
+ tx_omci_msg = request.fields['omci_message'].fields
+ rx_omci_msg = response.fields['omci_message'].fields
- if response.fields['omci_message'].fields['success_code'] != RC.Success:
- # TODO: Support offline ONTs in post VOLTHA v1.3.0
- omci_msg = response.fields['omci_message']
- self.log.warn('set-response-failure',
- class_id=omci_msg.fields['entity_class'],
- instance_id=omci_msg.fields['entity_id'],
- status=omci_msg.fields['success_code'],
- status_text=self._status_to_text(omci_msg.fields['success_code']),
- unsupported_attribute_mask=omci_msg.fields['unsupported_attributes_mask'],
- failed_attribute_mask=omci_msg.fields['failed_attributes_mask'])
+ rx_status = rx_omci_msg['success_code']
+ class_id = rx_omci_msg['entity_class']
+ entity_id = rx_omci_msg['entity_id']
+ attributes = dict()
+
+ tx_mask = tx_omci_msg['attributes_mask']
+ rx_fail_mask = 0
+ if rx_status == RC.AttributeFailure:
+ rx_fail_mask = rx_omci_msg['unsupported_attributes_mask'] | rx_omci_msg['failed_attributes_mask']
+
+ if rx_status == RC.Success:
+ attributes = {k: v for k, v in tx_omci_msg['data'].items()}
+
+ elif RC.AttributeFailure and tx_mask != rx_fail_mask:
+ # Partial success, set only those that were good
+ entity = self._device.me_map[class_id]
+ good_mask = tx_mask & ~rx_fail_mask
+ good_attr_indexes = entity.attribute_indices_from_mask(good_mask)
+ good_attr_names = {attr.field.name for index, attr in enumerate(entity.attributes)
+ if index in good_attr_indexes}
+
+ attributes = {k: v for k, v in tx_omci_msg['data'].items()
+ if k in good_attr_names}
else:
- omci_msg = request.fields['omci_message'].fields
- class_id = omci_msg['entity_class']
- entity_id = omci_msg['entity_id']
- attributes = {k: v for k, v in omci_msg['data'].items()}
+ self.log.warn('set-response-failure',
+ class_id=rx_omci_msg['entity_class'],
+ instance_id=rx_omci_msg['entity_id'],
+ status=rx_status,
+ status_text=self._status_to_text(rx_status),
+ unsupported_attribute_mask=rx_omci_msg['unsupported_attributes_mask'],
+ failed_attribute_mask=rx_omci_msg['failed_attributes_mask'])
- # Save to the database (Do not save 'sets' of the mib-data-sync however)
- if class_id != OntData.class_id:
- modified = self._database.set(self._device_id, class_id, entity_id, attributes)
- if modified:
- self.increment_mib_data_sync()
+ # Save to the database. A set of MDS in the OntData class results in
+ # an increment. However, we do not save that within the class/entity
+ # portion of the database.
+ if class_id == OntData.class_id and len(attributes) > 0:
+ self.increment_mib_data_sync()
+
+ elif len(attributes) > 0:
+ self._database.set(self._device_id, class_id, entity_id, attributes)
+ self.increment_mib_data_sync()
except KeyError as _e:
pass # NOP
except Exception as e:
self.log.exception('set', e=e)
+
+ def on_software_event(self, _topic, msg):
+ """
+ Process a Software Start, End, Activate, and Commit
+
+ :param _topic: (str) OMCI-RX topic
+ :param msg: (dict) Dictionary with 'rx-response' and 'tx-request' (if any)
+ """
+ self.log.debug('on-software-event', state=self.state)
+
+ # All events for software run this method, checking for one is good enough
+ if self._omci_cc_subscriptions[RxEvent.Start_Software_Download]:
+ if self.state in ['disabled', 'uploading']:
+ self.log.error('rx-in-invalid-state', state=self.state)
+ return
+ try:
+ # Note: all download responses we subscribe to have a 'result' field
+ response = msg[RX_RESPONSE_KEY]
+ if response.fields['omci_message'].fields['success_code'] == RC.Success:
+ self.increment_mib_data_sync()
+
+ except KeyError as _e:
+ pass # NOP
+ except Exception as e:
+ self.log.exception('set', e=e)
+
def on_capabilities_event(self, _topic, msg):
"""
Process a OMCI capabilties event
diff --git a/pyvoltha/adapters/extensions/omci/tasks/mib_reconcile_task.py b/pyvoltha/adapters/extensions/omci/tasks/mib_reconcile_task.py
index 8080274..421879b 100644
--- a/pyvoltha/adapters/extensions/omci/tasks/mib_reconcile_task.py
+++ b/pyvoltha/adapters/extensions/omci/tasks/mib_reconcile_task.py
@@ -615,6 +615,17 @@
returnValue((successes, failures))
@inlineCallbacks
+ def _get_current_mds(self):
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(OntDataFrame().get())
+
+ omci_msg = results.fields['omci_message'].fields
+ status = omci_msg['success_code']
+ mds = (omci_msg['data']['mib_data_sync'] >> 8) & 0xFF \
+ if status == 0 and 'data' in omci_msg and 'mib_data_sync' in omci_msg['data'] else -1
+ returnValue(mds)
+
+ @inlineCallbacks
def update_mib_data_sync(self):
"""
As the final step of MIB resynchronization, the OLT sets the MIB data sync
@@ -624,21 +635,27 @@
:return: (int, int) success, failure counts
"""
- # Get MDS to set, do not user zero
-
+ # Get MDS to set
+ self._sync_sm.increment_mib_data_sync()
new_mds_value = self._sync_sm.mib_data_sync
- if new_mds_value == 0:
- self._sync_sm.increment_mib_data_sync()
- new_mds_value = self._sync_sm.mib_data_sync
# Update it. The set response will be sent on the OMCI-CC pub/sub bus
# and the MIB Synchronizer will update this MDS value in the database
# if successful.
try:
+ # previous_mds = yield self._get_current_mds()
+
frame = OntDataFrame(mib_data_sync=new_mds_value).set()
results = yield self._device.omci_cc.send(frame)
self.check_status_and_state(results, 'ont-data-mbs-update')
+
+ #########################################
+ # Debug. Verify new MDS value was received. Should be 1 greater
+ # than what was sent
+ # new_mds = yield self._get_current_mds()
+ # self.log.info('mds-update', previous=previous_mds, new=new_mds_value, now=new_mds)
+ # Done
returnValue((1, 0))
except TimeoutError as e:
diff --git a/pyvoltha/adapters/extensions/omci/tasks/mib_resync_task.py b/pyvoltha/adapters/extensions/omci/tasks/mib_resync_task.py
index 13cb8ef..fdce7e6 100644
--- a/pyvoltha/adapters/extensions/omci/tasks/mib_resync_task.py
+++ b/pyvoltha/adapters/extensions/omci/tasks/mib_resync_task.py
@@ -18,8 +18,10 @@
from twisted.internet import reactor
from pyvoltha.common.utils.asleep import asleep
from pyvoltha.adapters.extensions.omci.database.mib_db_dict import *
-from pyvoltha.adapters.extensions.omci.omci_entities import OntData
+from pyvoltha.adapters.extensions.omci.omci_entities import OntData, Omci
from pyvoltha.adapters.extensions.omci.omci_defs import AttributeAccess, EntityOperations
+from pyvoltha.adapters.extensions.omci.omci_fields import OmciTableField
+from pyvoltha.adapters.extensions.omci.omci_me import OntDataFrame
AA = AttributeAccess
OP = EntityOperations
@@ -231,6 +233,13 @@
if number_of_commands is None or number_of_commands <= 0:
raise ValueError('Number of commands was {}'.format(number_of_commands))
+ # Get the current MIB-DATA-SYNC on the ONU
+ self.strobe_watchdog()
+ results = yield self._device.omci_cc.send(OntDataFrame().get())
+ omci_msg = results.fields['omci_message'].fields
+ mds = (omci_msg['data']['mib_data_sync'] >> 8) & 0xFF
+ self._db_active.save_mib_data_sync(self.device_id, mds)
+
returnValue(number_of_commands)
except TimeoutError as e:
@@ -259,7 +268,7 @@
# the device level and do not want it showing up during a re-sync
# during data comparison
from binascii import hexlify
- if class_id == OntData.class_id:
+ if class_id in (OntData.class_id, Omci.class_id):
break
# The T&W ONU reports an ME with class ID 0 but only on audit. Perhaps others do as well.
@@ -396,13 +405,16 @@
olt_cls = omci_copy[cls_id]
onu_cls = onu_copy[cls_id]
- # Weed out read-only attributes. Attributes on onu may be read-only. These
- # will only show up it the OpenOMCI (OLT-side) database if it changed and
- # an AVC Notification was sourced by the ONU
+ # Weed out read-only and table attributes. Attributes on onu may be read-only.
+ # These will only show up it the OpenOMCI (OLT-side) database if it changed
+ # and an AVC Notification was sourced by the ONU
# TODO: These class IDs could be calculated once at ONU startup (at device add)
if cls_id in me_map:
ro_attrs = {attr.field.name for attr in me_map[cls_id].attributes
if attr.access == ro_set}
+ table_attrs = {attr.field.name for attr in me_map[cls_id].attributes
+ if isinstance(attr.field, OmciTableField)}
+
else:
# Here if partially defined ME (not defined in ME Map)
from pyvoltha.adapters.extensions.omci.omci_cc import UNKNOWN_CLASS_ATTRIBUTE_KEY
diff --git a/pyvoltha/adapters/extensions/omci/tasks/omci_get_request.py b/pyvoltha/adapters/extensions/omci/tasks/omci_get_request.py
index 7950944..4f21154 100644
--- a/pyvoltha/adapters/extensions/omci/tasks/omci_get_request.py
+++ b/pyvoltha/adapters/extensions/omci/tasks/omci_get_request.py
@@ -18,9 +18,8 @@
from twisted.internet.defer import failure, inlineCallbacks, TimeoutError, returnValue
from pyvoltha.adapters.extensions.omci.omci_defs import ReasonCodes, EntityOperations
from pyvoltha.adapters.extensions.omci.omci_me import MEFrame
-from pyvoltha.adapters.extensions.omci.omci_frame import OmciFrame, OmciGetNext
-from pyvoltha.adapters.extensions.omci.omci_cc import DEFAULT_OMCI_TIMEOUT
-from pyvoltha.adapters.extensions.omci.omci_messages import OmciGet
+from pyvoltha.adapters.extensions.omci.omci_frame import OmciFrame
+from pyvoltha.adapters.extensions.omci.omci_messages import OmciGet, OmciGetNext
from pyvoltha.adapters.extensions.omci.omci_fields import OmciTableField
RC = ReasonCodes
@@ -151,6 +150,28 @@
attr_def = self._entity_class.attributes[index]
return isinstance(attr_def.field, OmciTableField)
+ def select_first_attributes(self):
+ """
+ For the requested attributes, get as many as possible in the first requst
+ :return: (set) attributes that will fit in one frame
+ """
+ octets_available = 25 # Max GET Response baseline payload size
+ first_attributes = set()
+
+ for attr in self._attributes:
+ index = self._entity_class.attribute_name_to_index_map[attr]
+ attr_def = self._entity_class.attributes[index]
+
+ if isinstance(attr_def.field, OmciTableField):
+ continue # No table attributes
+
+ size = attr_def.field.sz
+ if size <= octets_available:
+ first_attributes.add(attr)
+ octets_available -= size
+
+ return first_attributes
+
@inlineCallbacks
def perform_get_omci(self):
"""
@@ -160,8 +181,7 @@
entity_id=self._entity_id, attributes=self._attributes)
try:
# If one or more attributes is a table attribute, get it separately
-
- first_attributes = {attr for attr in self._attributes if not self.is_table_attr(attr)}
+ first_attributes = self.select_first_attributes()
table_attributes = {attr for attr in self._attributes if self.is_table_attr(attr)}
if len(first_attributes):
@@ -189,10 +209,14 @@
results_omci = results.fields['omci_message'].fields
# Were all attributes fetched?
- missing_attr = frame.fields['omci_message'].fields['attributes_mask'] ^ \
- results_omci['attributes_mask']
+ requested_attr = frame.fields['omci_message'].fields['attributes_mask']
+ retrieved_attr = results_omci['attributes_mask']
+ unsupported_attr = results_omci.get('unsupported_attributes_mask', 0) or 0
+ failed_attr = results_omci.get('failed_attributes_mask', 0) or 0
+ not_avail_attr = unsupported_attr | failed_attr
+ missing_attr = requested_attr & ~(retrieved_attr | not_avail_attr)
- if missing_attr > 0 or len(table_attributes) > 0:
+ if missing_attr != 0 or len(table_attributes) > 0:
self.log.info('perform-get-missing', num_missing=missing_attr,
table_attr=table_attributes)
self.strobe_watchdog()
@@ -268,16 +292,16 @@
status = get_omci['success_code']
if status == RC.AttributeFailure.value:
- # TODO: update failed & unknown attributes set
+ unsupported_attr = get_omci.get('unsupported_attributes_mask', 0) or 0
+ failed_attr = get_omci.get('failed_attributes_mask', 0) or 0
+ results_omci['unsupported_attributes_mask'] |= unsupported_attr
+ results_omci['failed_attributes_mask'] |= failed_attr
continue
elif status != RC.Success.value:
raise GetException('Get failed with status code: {}'.format(status))
- # assert attr_mask == get_omci['attributes_mask'], 'wrong attribute'
- if attr_mask != get_omci['attributes_mask']:
- self.log.debug('attr mask does not match expected mask', attr_mask=attr_mask,
- expected_mask = get_omci['attributes_mask'])
+ assert attr_mask == get_omci['attributes_mask'], 'wrong attribute'
results_omci['attributes_mask'] |= attr_mask
if results_omci.get('data') is None:
@@ -297,8 +321,7 @@
# Now any table attributes
if len(table_attributes):
self.strobe_watchdog()
- self._local_deferred = reactor.callLater(0,
- self.process_get_table,
+ self._local_deferred = reactor.callLater(0, self.process_get_table,
table_attributes)
returnValue(self._local_deferred)
@@ -339,8 +362,10 @@
if omci_fields['success_code'] == RC.AttributeFailure.value:
# Copy over any failed or unsupported attribute masks for final result
results_fields = results_omci.fields['omci_message'].fields
- results_fields['unsupported_attributes_mask'] |= omci_fields['unsupported_attributes_mask']
- results_fields['failed_attributes_mask'] |= omci_fields['failed_attributes_mask']
+ unsupported_attr = results_omci.get('unsupported_attributes_mask', 0) or 0
+ failed_attr = results_omci.get('failed_attributes_mask', 0) or 0
+ results_fields['unsupported_attributes_mask'] |= unsupported_attr
+ results_fields['failed_attributes_mask'] |= failed_attr
if omci_fields['success_code'] != RC.Success.value:
raise GetException('Get table attribute failed with status code: {}'.
diff --git a/test/unit/extensions/omci/test_mib_resync_task.py b/test/unit/extensions/omci/test_mib_resync_task.py
index 43b27d9..892cd97 100644
--- a/test/unit/extensions/omci/test_mib_resync_task.py
+++ b/test/unit/extensions/omci/test_mib_resync_task.py
@@ -303,11 +303,11 @@
class_id = PriorityQueueG.class_id
inst_id = 0
attributes_olt = {
- 'related_port': int(1234), # IntField (R/O)
+ 'related_port': int(1234), # IntField (R/W)
'maximum_queue_size': int(222) # Only on OLT but read-only
}
attributes_onu = {
- 'related_port': int(5678) # IntField (R/O)
+ 'related_port': int(1234) # IntField (R/W)
}
self.onu_db.set(_DEVICE_ID, class_id, inst_id, attributes_onu)
self.olt_db.set(_DEVICE_ID, class_id, inst_id, attributes_olt)
diff --git a/test/unit/extensions/omci/test_omci.py b/test/unit/extensions/omci/test_omci.py
index 6df072b..b6397a8 100644
--- a/test/unit/extensions/omci/test_omci.py
+++ b/test/unit/extensions/omci/test_omci.py
@@ -14,6 +14,7 @@
# limitations under the License.
#
from unittest import TestCase, main
+from binascii import unhexlify
from pyvoltha.adapters.extensions.omci.omci import *
@@ -1136,7 +1137,7 @@
message_type=OmciReboot.message_id,
omci_message=OmciReboot(
entity_class=OntG.class_id,
- entity_id=0
+ entity_id=0
)
)
self.assertGeneratedFrameEquals(frame, ref)
@@ -1157,6 +1158,128 @@
self.assertTrue(AA.SBC not in mei_attr.access or
mei_attr.field.name == 'managed_entity_id')
+ def test_get_response_without_error_but_too_big(self):
+ # This test is related to a bug that I believe is in the BroadCom
+ # ONU stack software, or at least it was seen on both an Alpha and
+ # an T&W BCM-based onu. The IEEE 802.1p Mapper Service Profile ME
+ # (#130) sent by the ONUs have a payload of 27 octets based on the
+ # Attribute Mask in the encoding. However, get-response baseline
+ # messages have the last 4 octets reserved for failed/errored attribute
+ # masks so only 25 octets should be allowed. Of course the 4 octets
+ # are only valid if the status code == 9, but they still should
+ # be reserved.
+ #
+ # This test verifies that we can still parse the 27 octet payload
+ # since the first rule of interoperability is to be lenient with
+ # what you receive and strict with what you transmit.
+ #
+ ref = '017d290a008280020000780000000000000000000000' +\
+ '0000000000000000000000000000' +\
+ '01' +\
+ '02' +\
+ '0000' +\
+ '00000028'
+ zeros_24 = '000000000000000000000000000000000000000000000000'
+ bytes_24 = unhexlify(zeros_24)
+ attributes = {
+ "unmarked_frame_option": 0, # 1 octet
+ "dscp_to_p_bit_mapping": bytes_24, # 24 octets
+ "default_p_bit_marking": 1, # 1 octet - This is too much
+ "tp_type": 2, # 1 octet
+ }
+ frame = OmciFrame(
+ transaction_id=0x017d,
+ message_type=OmciGetResponse.message_id,
+ omci_message=OmciGetResponse(
+ entity_class=Ieee8021pMapperServiceProfile.class_id,
+ success_code=0,
+ entity_id=0x8002,
+ attributes_mask=Ieee8021pMapperServiceProfile.mask_for(*attributes.keys()),
+ data=attributes
+ )
+ )
+ self.assertGeneratedFrameEquals(frame, ref)
+
+ def test_get_response_with_errors_max_data(self):
+ # First a frame with maximum data used up. This aligns the fields up perfectly
+ # with the simplest definition of a Get Response
+ ref = '017d290a008280020900600000000000000000000000' +\
+ '0000000000000000000000000000' +\
+ '0010' +\
+ '0008' +\
+ '00000028'
+ zeros_24 = '000000000000000000000000000000000000000000000000'
+ bytes_24 = unhexlify(zeros_24)
+ good_attributes = {
+ "unmarked_frame_option": 0, # 1 octet
+ "dscp_to_p_bit_mapping": bytes_24, # 24 octets
+ }
+ unsupported_attributes = ["default_p_bit_marking"]
+ failed_attributes_mask = ["tp_type"]
+
+ the_class = Ieee8021pMapperServiceProfile
+ frame = OmciFrame(
+ transaction_id=0x017d,
+ message_type=OmciGetResponse.message_id,
+ omci_message=OmciGetResponse(
+ entity_class=the_class.class_id,
+ success_code=9,
+ entity_id=0x8002,
+ attributes_mask=the_class.mask_for(*good_attributes.keys()),
+ unsupported_attributes_mask=the_class.mask_for(*unsupported_attributes),
+ failed_attributes_mask=the_class.mask_for(*failed_attributes_mask),
+ data=good_attributes
+ )
+ )
+ self.assertGeneratedFrameEquals(frame, ref)
+
+ def test_get_response_with_errors_min_data(self):
+ # Next a frame with only a little data used up. This aligns will require
+ # the encoder and decoder to skip to the last 8 octets of the data field
+ # and encode the failed masks there
+ ref = '017d290a00828002090040' +\
+ '01' + '00000000000000000000' +\
+ '0000000000000000000000000000' +\
+ '0010' +\
+ '0028' +\
+ '00000028'
+
+ good_attributes = {
+ "unmarked_frame_option": 1, # 1 octet
+ }
+ unsupported_attributes = ["default_p_bit_marking"]
+ failed_attributes_mask = ["dscp_to_p_bit_mapping", "tp_type"]
+
+ the_class = Ieee8021pMapperServiceProfile
+ frame = OmciFrame(
+ transaction_id=0x017d,
+ message_type=OmciGetResponse.message_id,
+ omci_message=OmciGetResponse(
+ entity_class=the_class.class_id,
+ success_code=9,
+ entity_id=0x8002,
+ attributes_mask=the_class.mask_for(*good_attributes.keys()),
+ unsupported_attributes_mask=the_class.mask_for(*unsupported_attributes),
+ failed_attributes_mask=the_class.mask_for(*failed_attributes_mask),
+ data=good_attributes
+ )
+ )
+ self.assertGeneratedFrameEquals(frame, ref)
+
+ # Now test decode of the packet
+ decoded = OmciFrame(unhexlify(ref))
+
+ orig_fields = frame.fields['omci_message'].fields
+ omci_fields = decoded.fields['omci_message'].fields
+
+ for field in ['entity_class', 'entity_id', 'attributes_mask',
+ 'success_code', 'unsupported_attributes_mask',
+ 'failed_attributes_mask']:
+ self.assertEqual(omci_fields[field], orig_fields[field])
+
+ self.assertEqual(omci_fields['data']['unmarked_frame_option'],
+ orig_fields['data']['unmarked_frame_option'])
+
if __name__ == '__main__':
main()