blob: ef9c53162089eaba50210d2f6d41b65b9b121390 [file] [log] [blame]
#
# 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 task import Task
from twisted.internet.defer import inlineCallbacks, TimeoutError, failure, returnValue
from twisted.internet import reactor
from common.utils.asleep import asleep
from voltha.extensions.omci.database.mib_db_dict import *
from voltha.extensions.omci.omci_entities import OntData
from voltha.extensions.omci.omci_defs import AttributeAccess, EntityOperations
AA = AttributeAccess
OP = EntityOperations
class MibCopyException(Exception):
pass
class MibDownloadException(Exception):
pass
class MibResyncException(Exception):
pass
class MibResyncTask(Task):
"""
OpenOMCI MIB resynchronization Task
This task should get a copy of the MIB and compare compare it to a
copy of the database. When the MIB Upload command is sent to the ONU,
it should make a copy and source the data requested from this database.
The ONU can still source AVC's and the the OLT can still send config
commands to the actual.
"""
task_priority = 240
name = "MIB Resynchronization Task"
max_db_copy_retries = 3
db_copy_retry_delay = 7
max_mib_upload_next_retries = 3
mib_upload_next_delay = 10 # Max * delay < 60 seconds
watchdog_timeout = 15 # Should be > max delay
def __init__(self, omci_agent, device_id):
"""
Class initialization
:param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
:param device_id: (str) ONU Device ID
"""
super(MibResyncTask, self).__init__(MibResyncTask.name,
omci_agent,
device_id,
priority=MibResyncTask.task_priority,
exclusive=False)
self._local_deferred = None
self._device = omci_agent.get_device(device_id)
self._db_active = MibDbVolatileDict(omci_agent)
self._db_active.start()
def cancel_deferred(self):
super(MibResyncTask, 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 MIB Re-Synchronization task
"""
super(MibResyncTask, self).start()
self._local_deferred = reactor.callLater(0, self.perform_mib_resync)
self._db_active.start()
self._db_active.add(self.device_id)
def stop(self):
"""
Shutdown MIB Re-Synchronization task
"""
self.log.debug('stopping')
self.cancel_deferred()
self._device = None
self._db_active.stop()
self._db_active = None
super(MibResyncTask, self).stop()
@inlineCallbacks
def perform_mib_resync(self):
"""
Perform the MIB Resynchronization sequence
The sequence to be performed is:
- get a copy of the current MIB database (db_copy)
- perform MIB upload commands to get ONU's database and save this
to a local DB (db_active). Note that the ONU can still receive
create/delete/set/get operations from the operator and source
AVC notifications as well during this period.
- Compare the information in the db_copy to the db_active
During the mib upload process, the maximum time between mib upload next
requests is 1 minute.
"""
self.log.debug('perform-mib-resync')
try:
results = yield self.snapshot_mib()
db_copy = results[0]
if db_copy is None:
e = MibCopyException('Failed to get local database copy')
self.deferred.errback(failure.Failure(e))
else:
number_of_commands = results[1]
# Start the MIB upload sequence
self.strobe_watchdog()
commands_retrieved = yield self.upload_mib(number_of_commands)
if commands_retrieved < number_of_commands:
e = MibDownloadException('Only retrieved {} of {} instances'.
format(commands_retrieved, number_of_commands))
self.deferred.errback(failure.Failure(e))
else:
# Compare the databases
active_copy = self._db_active.query(self.device_id)
on_olt_only, on_onu_only, attr_diffs = \
self.compare_mibs(db_copy, active_copy)
self.deferred.callback(
{
'on-olt-only': on_olt_only if len(on_olt_only) else None,
'on-onu-only': on_onu_only if len(on_onu_only) else None,
'attr-diffs': attr_diffs if len(attr_diffs) else None,
'olt-db': db_copy,
'onu-db': active_copy
})
except Exception as e:
self.log.exception('resync', e=e)
self.deferred.errback(failure.Failure(e))
@inlineCallbacks
def snapshot_mib(self):
"""
Snapshot the MIB on the ONU and create a copy of our local MIB database
:return: (pair) (db_copy, number_of_commands)
"""
db_copy = None
number_of_commands = None
try:
max_tries = MibResyncTask.max_db_copy_retries - 1
for retries in xrange(0, max_tries + 1):
# Send MIB Upload so ONU snapshots its MIB
try:
self.strobe_watchdog()
number_of_commands = yield self.send_mib_upload()
if number_of_commands is None:
if retries >= max_tries:
db_copy = None
break
except (TimeoutError, ValueError) as e:
self.log.warn('timeout-or-value-error', e=e)
if retries >= max_tries:
raise
self.strobe_watchdog()
yield asleep(MibResyncTask.db_copy_retry_delay)
continue
# Get a snapshot of the local MIB database
db_copy = self._device.query_mib()
# if we made it this far, no need to keep trying
break
except Exception as e:
self.log.exception('mib-resync', e=e)
raise
# Handle initial failures
if db_copy is None or number_of_commands is None:
raise MibCopyException('Failed to snapshot MIB copy after {} retries'.
format(MibResyncTask.max_db_copy_retries))
returnValue((db_copy, number_of_commands))
@inlineCallbacks
def send_mib_upload(self):
"""
Perform MIB upload command and get the number of entries to retrieve
:return: (int) Number of commands to execute or None on error
"""
########################################
# Begin MIB Upload
try:
self.strobe_watchdog()
results = yield self._device.omci_cc.send_mib_upload()
number_of_commands = results.fields['omci_message'].fields['number_of_commands']
if number_of_commands is None or number_of_commands <= 0:
raise ValueError('Number of commands was {}'.format(number_of_commands))
returnValue(number_of_commands)
except TimeoutError as e:
self.log.warn('mib-resync-get-timeout', e=e)
raise
@inlineCallbacks
def upload_mib(self, number_of_commands):
########################################
# Begin MIB Upload
seq_no = None
for seq_no in xrange(number_of_commands):
max_tries = MibResyncTask.max_mib_upload_next_retries
for retries in xrange(0, max_tries):
try:
self.strobe_watchdog()
response = yield self._device.omci_cc.send_mib_upload_next(seq_no)
omci_msg = response.fields['omci_message'].fields
class_id = omci_msg['object_entity_class']
entity_id = omci_msg['object_entity_id']
# Filter out the 'mib_data_sync' from the database. We save that at
# 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:
break
# The T&W ONU reports an ME with class ID 0 but only on audit. Perhaps others do as well.
if class_id == 0 or class_id > 0xFFFF:
self.log.warn('invalid-class-id', class_id=class_id)
break
attributes = {k: v for k, v in omci_msg['object_data'].items()}
# Save to the database
self._db_active.set(self.device_id, class_id, entity_id, attributes)
break
except TimeoutError:
self.log.warn('mib-resync-timeout', seq_no=seq_no,
number_of_commands=number_of_commands)
if retries < max_tries - 1:
self.strobe_watchdog()
yield asleep(MibResyncTask.mib_upload_next_delay)
else:
raise
except Exception as e:
self.log.exception('resync', e=e, seq_no=seq_no,
number_of_commands=number_of_commands)
returnValue(seq_no + 1) # seq_no is zero based.
def compare_mibs(self, db_copy, db_active):
"""
Compare the our db_copy with the ONU's active copy
:param db_copy: (dict) OpenOMCI's copy of the database
:param db_active: (dict) ONU's database snapshot
:return: (dict), (dict), (list) Differences
"""
self.strobe_watchdog()
me_map = self.omci_agent.get_device(self.device_id).me_map
# Class & Entities only in local copy (OpenOMCI)
on_olt_temp = self.get_lhs_only_dict(db_copy, db_active)
# Remove any entries that are not reported during an upload (but could
# be in our database copy. Retain undecodable class IDs.
on_olt_only = [(cid, eid) for cid, eid in on_olt_temp
if cid not in me_map or not me_map[cid].hidden]
# Further reduce the on_olt_only MEs reported in an audit to not
# include missed MEs that are ONU created. Not all ONUs report MEs
# that are ONU created unless we are doing the initial MIB upload.
# Adtran does report them, T&W may not as well as a few others
on_olt_only = [(cid, eid) for cid, eid in on_olt_only if cid in me_map and
(OP.Create in me_map[cid].mandatory_operations or
OP.Create in me_map[cid].optional_operations)]
# Class & Entities only on remote (ONU)
on_onu_only = self.get_lhs_only_dict(db_active, db_copy)
# Class & Entities on both local & remote, but one or more attributes
# are different on the ONU. This is the value that the local (OpenOMCI)
# thinks should be on the remote (ONU)
attr_diffs = self.get_attribute_diffs(db_copy, db_active, me_map)
# TODO: Note that certain MEs are excluded from the MIB upload. In particular,
# instances of some general purpose MEs, such as the Managed Entity ME and
# and the Attribute ME are not included in the MIB upload. Also all table
# attributes are not included in the MIB upload (but we do not yet support
# tables in this OpenOMCI implementation (VOLTHA v1.3.0)
return on_olt_only, on_onu_only, attr_diffs
def get_lhs_only_dict(self, lhs, rhs):
"""
Compare two MIB database dictionaries and return the ME Class ID and
instances that are unique to the lhs dictionary. Both parameters
should be in the common MIB Database output dictionary format that
is returned by the mib 'query' command.
:param lhs: (dict) Left-hand-side argument.
:param rhs: (dict) Right-hand-side argument
return: (list(int,int)) List of tuples where (class_id, inst_id)
"""
results = list()
for cls_id, cls_data in lhs.items():
# Get unique classes
#
# Skip keys that are not class IDs
if not isinstance(cls_id, int):
continue
if cls_id not in rhs:
results.extend([(cls_id, inst_id) for inst_id in cls_data.keys()
if isinstance(inst_id, int)])
else:
# Get unique instances of a class
lhs_cls = cls_data
rhs_cls = rhs[cls_id]
for inst_id, _ in lhs_cls.items():
# Skip keys that are not instance IDs
if isinstance(cls_id, int) and inst_id not in rhs_cls:
results.extend([(cls_id, inst_id)])
return results
def get_attribute_diffs(self, omci_copy, onu_copy, me_map):
"""
Compare two OMCI MIBs and return the ME class and instance IDs that exists
on both the local copy and the remote ONU that have different attribute
values. Both parameters should be in the common MIB Database output
dictionary format that is returned by the mib 'query' command.
:param omci_copy: (dict) OpenOMCI copy (OLT-side) of the MIB Database
:param onu_copy: (dict) active ONU latest copy its database
:param me_map: (dict) ME Class ID MAP for this ONU
return: (list(int,int,str)) List of tuples where (class_id, inst_id, attribute)
points to the specific ME instance where attributes
are different
"""
results = list()
ro_set = {AA.R}
# Get class ID's that are in both
class_ids = {cls_id for cls_id, _ in omci_copy.items()
if isinstance(cls_id, int) and cls_id in onu_copy}
for cls_id in class_ids:
# Get unique instances of a class
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
# 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}
else:
# Here if partially defined ME (not defined in ME Map)
from voltha.extensions.omci.omci_cc import UNKNOWN_CLASS_ATTRIBUTE_KEY
ro_attrs = {UNKNOWN_CLASS_ATTRIBUTE_KEY}
# Get set of common instance IDs
inst_ids = {inst_id for inst_id, _ in olt_cls.items()
if isinstance(inst_id, int) and inst_id in onu_cls}
for inst_id in inst_ids:
omci_attributes = {k for k in olt_cls[inst_id][ATTRIBUTES_KEY].iterkeys()}
onu_attributes = {k for k in onu_cls[inst_id][ATTRIBUTES_KEY].iterkeys()}
# Get attributes that exist in one database, but not the other
sym_diffs = (omci_attributes ^ onu_attributes) - ro_attrs
results.extend([(cls_id, inst_id, attr) for attr in sym_diffs])
# Get common attributes with different values
common_attributes = (omci_attributes & onu_attributes) - ro_attrs
results.extend([(cls_id, inst_id, attr) for attr in common_attributes
if olt_cls[inst_id][ATTRIBUTES_KEY][attr] !=
onu_cls[inst_id][ATTRIBUTES_KEY][attr]])
return results