blob: ef9c53162089eaba50210d2f6d41b65b9b121390 [file] [log] [blame]
Chip Boling32aab302019-01-23 10:50:18 -06001#
2# Copyright 2017 the original author or authors.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16from task import Task
17from twisted.internet.defer import inlineCallbacks, TimeoutError, failure, returnValue
18from twisted.internet import reactor
19from common.utils.asleep import asleep
20from voltha.extensions.omci.database.mib_db_dict import *
21from voltha.extensions.omci.omci_entities import OntData
22from voltha.extensions.omci.omci_defs import AttributeAccess, EntityOperations
23
24AA = AttributeAccess
25OP = EntityOperations
26
27class MibCopyException(Exception):
28 pass
29
30
31class MibDownloadException(Exception):
32 pass
33
34
35class MibResyncException(Exception):
36 pass
37
38
39class MibResyncTask(Task):
40 """
41 OpenOMCI MIB resynchronization Task
42
43 This task should get a copy of the MIB and compare compare it to a
44 copy of the database. When the MIB Upload command is sent to the ONU,
45 it should make a copy and source the data requested from this database.
46 The ONU can still source AVC's and the the OLT can still send config
47 commands to the actual.
48 """
49 task_priority = 240
50 name = "MIB Resynchronization Task"
51
52 max_db_copy_retries = 3
53 db_copy_retry_delay = 7
54
55 max_mib_upload_next_retries = 3
56 mib_upload_next_delay = 10 # Max * delay < 60 seconds
57 watchdog_timeout = 15 # Should be > max delay
58
59 def __init__(self, omci_agent, device_id):
60 """
61 Class initialization
62
63 :param omci_agent: (OpenOMCIAgent) OMCI Adapter agent
64 :param device_id: (str) ONU Device ID
65 """
66 super(MibResyncTask, self).__init__(MibResyncTask.name,
67 omci_agent,
68 device_id,
69 priority=MibResyncTask.task_priority,
70 exclusive=False)
71 self._local_deferred = None
72 self._device = omci_agent.get_device(device_id)
73 self._db_active = MibDbVolatileDict(omci_agent)
74 self._db_active.start()
75
76 def cancel_deferred(self):
77 super(MibResyncTask, self).cancel_deferred()
78
79 d, self._local_deferred = self._local_deferred, None
80 try:
81 if d is not None and not d.called:
82 d.cancel()
83 except:
84 pass
85
86 def start(self):
87 """
88 Start MIB Re-Synchronization task
89 """
90 super(MibResyncTask, self).start()
91 self._local_deferred = reactor.callLater(0, self.perform_mib_resync)
92 self._db_active.start()
93 self._db_active.add(self.device_id)
94
95 def stop(self):
96 """
97 Shutdown MIB Re-Synchronization task
98 """
99 self.log.debug('stopping')
100
101 self.cancel_deferred()
102 self._device = None
103 self._db_active.stop()
104 self._db_active = None
105 super(MibResyncTask, self).stop()
106
107 @inlineCallbacks
108 def perform_mib_resync(self):
109 """
110 Perform the MIB Resynchronization sequence
111
112 The sequence to be performed is:
113 - get a copy of the current MIB database (db_copy)
114
115 - perform MIB upload commands to get ONU's database and save this
116 to a local DB (db_active). Note that the ONU can still receive
117 create/delete/set/get operations from the operator and source
118 AVC notifications as well during this period.
119
120 - Compare the information in the db_copy to the db_active
121
122 During the mib upload process, the maximum time between mib upload next
123 requests is 1 minute.
124 """
125 self.log.debug('perform-mib-resync')
126
127 try:
128 results = yield self.snapshot_mib()
129 db_copy = results[0]
130
131 if db_copy is None:
132 e = MibCopyException('Failed to get local database copy')
133 self.deferred.errback(failure.Failure(e))
134
135 else:
136 number_of_commands = results[1]
137
138 # Start the MIB upload sequence
139 self.strobe_watchdog()
140 commands_retrieved = yield self.upload_mib(number_of_commands)
141
142 if commands_retrieved < number_of_commands:
143 e = MibDownloadException('Only retrieved {} of {} instances'.
144 format(commands_retrieved, number_of_commands))
145 self.deferred.errback(failure.Failure(e))
146 else:
147 # Compare the databases
148 active_copy = self._db_active.query(self.device_id)
149 on_olt_only, on_onu_only, attr_diffs = \
150 self.compare_mibs(db_copy, active_copy)
151
152 self.deferred.callback(
153 {
154 'on-olt-only': on_olt_only if len(on_olt_only) else None,
155 'on-onu-only': on_onu_only if len(on_onu_only) else None,
156 'attr-diffs': attr_diffs if len(attr_diffs) else None,
157 'olt-db': db_copy,
158 'onu-db': active_copy
159 })
160
161 except Exception as e:
162 self.log.exception('resync', e=e)
163 self.deferred.errback(failure.Failure(e))
164
165 @inlineCallbacks
166 def snapshot_mib(self):
167 """
168 Snapshot the MIB on the ONU and create a copy of our local MIB database
169
170 :return: (pair) (db_copy, number_of_commands)
171 """
172 db_copy = None
173 number_of_commands = None
174
175 try:
176 max_tries = MibResyncTask.max_db_copy_retries - 1
177
178 for retries in xrange(0, max_tries + 1):
179 # Send MIB Upload so ONU snapshots its MIB
180 try:
181 self.strobe_watchdog()
182 number_of_commands = yield self.send_mib_upload()
183
184 if number_of_commands is None:
185 if retries >= max_tries:
186 db_copy = None
187 break
188
189 except (TimeoutError, ValueError) as e:
190 self.log.warn('timeout-or-value-error', e=e)
191 if retries >= max_tries:
192 raise
193
194 self.strobe_watchdog()
195 yield asleep(MibResyncTask.db_copy_retry_delay)
196 continue
197
198 # Get a snapshot of the local MIB database
199 db_copy = self._device.query_mib()
200 # if we made it this far, no need to keep trying
201 break
202
203 except Exception as e:
204 self.log.exception('mib-resync', e=e)
205 raise
206
207 # Handle initial failures
208
209 if db_copy is None or number_of_commands is None:
210 raise MibCopyException('Failed to snapshot MIB copy after {} retries'.
211 format(MibResyncTask.max_db_copy_retries))
212
213 returnValue((db_copy, number_of_commands))
214
215 @inlineCallbacks
216 def send_mib_upload(self):
217 """
218 Perform MIB upload command and get the number of entries to retrieve
219
220 :return: (int) Number of commands to execute or None on error
221 """
222 ########################################
223 # Begin MIB Upload
224 try:
225 self.strobe_watchdog()
226 results = yield self._device.omci_cc.send_mib_upload()
227
228 number_of_commands = results.fields['omci_message'].fields['number_of_commands']
229
230 if number_of_commands is None or number_of_commands <= 0:
231 raise ValueError('Number of commands was {}'.format(number_of_commands))
232
233 returnValue(number_of_commands)
234
235 except TimeoutError as e:
236 self.log.warn('mib-resync-get-timeout', e=e)
237 raise
238
239 @inlineCallbacks
240 def upload_mib(self, number_of_commands):
241 ########################################
242 # Begin MIB Upload
243 seq_no = None
244
245 for seq_no in xrange(number_of_commands):
246 max_tries = MibResyncTask.max_mib_upload_next_retries
247
248 for retries in xrange(0, max_tries):
249 try:
250 self.strobe_watchdog()
251 response = yield self._device.omci_cc.send_mib_upload_next(seq_no)
252
253 omci_msg = response.fields['omci_message'].fields
254 class_id = omci_msg['object_entity_class']
255 entity_id = omci_msg['object_entity_id']
256
257 # Filter out the 'mib_data_sync' from the database. We save that at
258 # the device level and do not want it showing up during a re-sync
259 # during data comparison
260 from binascii import hexlify
261 if class_id == OntData.class_id:
262 break
263
264 # The T&W ONU reports an ME with class ID 0 but only on audit. Perhaps others do as well.
265 if class_id == 0 or class_id > 0xFFFF:
266 self.log.warn('invalid-class-id', class_id=class_id)
267 break
268
269 attributes = {k: v for k, v in omci_msg['object_data'].items()}
270
271 # Save to the database
272 self._db_active.set(self.device_id, class_id, entity_id, attributes)
273 break
274
275 except TimeoutError:
276 self.log.warn('mib-resync-timeout', seq_no=seq_no,
277 number_of_commands=number_of_commands)
278
279 if retries < max_tries - 1:
280 self.strobe_watchdog()
281 yield asleep(MibResyncTask.mib_upload_next_delay)
282 else:
283 raise
284
285 except Exception as e:
286 self.log.exception('resync', e=e, seq_no=seq_no,
287 number_of_commands=number_of_commands)
288
289 returnValue(seq_no + 1) # seq_no is zero based.
290
291 def compare_mibs(self, db_copy, db_active):
292 """
293 Compare the our db_copy with the ONU's active copy
294
295 :param db_copy: (dict) OpenOMCI's copy of the database
296 :param db_active: (dict) ONU's database snapshot
297 :return: (dict), (dict), (list) Differences
298 """
299 self.strobe_watchdog()
300 me_map = self.omci_agent.get_device(self.device_id).me_map
301
302 # Class & Entities only in local copy (OpenOMCI)
303 on_olt_temp = self.get_lhs_only_dict(db_copy, db_active)
304
305 # Remove any entries that are not reported during an upload (but could
306 # be in our database copy. Retain undecodable class IDs.
307 on_olt_only = [(cid, eid) for cid, eid in on_olt_temp
308 if cid not in me_map or not me_map[cid].hidden]
309
310 # Further reduce the on_olt_only MEs reported in an audit to not
311 # include missed MEs that are ONU created. Not all ONUs report MEs
312 # that are ONU created unless we are doing the initial MIB upload.
313 # Adtran does report them, T&W may not as well as a few others
314 on_olt_only = [(cid, eid) for cid, eid in on_olt_only if cid in me_map and
315 (OP.Create in me_map[cid].mandatory_operations or
316 OP.Create in me_map[cid].optional_operations)]
317
318 # Class & Entities only on remote (ONU)
319 on_onu_only = self.get_lhs_only_dict(db_active, db_copy)
320
321 # Class & Entities on both local & remote, but one or more attributes
322 # are different on the ONU. This is the value that the local (OpenOMCI)
323 # thinks should be on the remote (ONU)
324
325 attr_diffs = self.get_attribute_diffs(db_copy, db_active, me_map)
326
327 # TODO: Note that certain MEs are excluded from the MIB upload. In particular,
328 # instances of some general purpose MEs, such as the Managed Entity ME and
329 # and the Attribute ME are not included in the MIB upload. Also all table
330 # attributes are not included in the MIB upload (but we do not yet support
331 # tables in this OpenOMCI implementation (VOLTHA v1.3.0)
332
333 return on_olt_only, on_onu_only, attr_diffs
334
335 def get_lhs_only_dict(self, lhs, rhs):
336 """
337 Compare two MIB database dictionaries and return the ME Class ID and
338 instances that are unique to the lhs dictionary. Both parameters
339 should be in the common MIB Database output dictionary format that
340 is returned by the mib 'query' command.
341
342 :param lhs: (dict) Left-hand-side argument.
343 :param rhs: (dict) Right-hand-side argument
344
345 return: (list(int,int)) List of tuples where (class_id, inst_id)
346 """
347 results = list()
348
349 for cls_id, cls_data in lhs.items():
350 # Get unique classes
351 #
352 # Skip keys that are not class IDs
353 if not isinstance(cls_id, int):
354 continue
355
356 if cls_id not in rhs:
357 results.extend([(cls_id, inst_id) for inst_id in cls_data.keys()
358 if isinstance(inst_id, int)])
359 else:
360 # Get unique instances of a class
361 lhs_cls = cls_data
362 rhs_cls = rhs[cls_id]
363
364 for inst_id, _ in lhs_cls.items():
365 # Skip keys that are not instance IDs
366 if isinstance(cls_id, int) and inst_id not in rhs_cls:
367 results.extend([(cls_id, inst_id)])
368
369 return results
370
371 def get_attribute_diffs(self, omci_copy, onu_copy, me_map):
372 """
373 Compare two OMCI MIBs and return the ME class and instance IDs that exists
374 on both the local copy and the remote ONU that have different attribute
375 values. Both parameters should be in the common MIB Database output
376 dictionary format that is returned by the mib 'query' command.
377
378 :param omci_copy: (dict) OpenOMCI copy (OLT-side) of the MIB Database
379 :param onu_copy: (dict) active ONU latest copy its database
380 :param me_map: (dict) ME Class ID MAP for this ONU
381
382 return: (list(int,int,str)) List of tuples where (class_id, inst_id, attribute)
383 points to the specific ME instance where attributes
384 are different
385 """
386 results = list()
387 ro_set = {AA.R}
388
389 # Get class ID's that are in both
390 class_ids = {cls_id for cls_id, _ in omci_copy.items()
391 if isinstance(cls_id, int) and cls_id in onu_copy}
392
393 for cls_id in class_ids:
394 # Get unique instances of a class
395 olt_cls = omci_copy[cls_id]
396 onu_cls = onu_copy[cls_id]
397
398 # Weed out read-only attributes. Attributes on onu may be read-only. These
399 # will only show up it the OpenOMCI (OLT-side) database if it changed and
400 # an AVC Notification was sourced by the ONU
401 # TODO: These class IDs could be calculated once at ONU startup (at device add)
402 if cls_id in me_map:
403 ro_attrs = {attr.field.name for attr in me_map[cls_id].attributes
404 if attr.access == ro_set}
405 else:
406 # Here if partially defined ME (not defined in ME Map)
407 from voltha.extensions.omci.omci_cc import UNKNOWN_CLASS_ATTRIBUTE_KEY
408 ro_attrs = {UNKNOWN_CLASS_ATTRIBUTE_KEY}
409
410 # Get set of common instance IDs
411 inst_ids = {inst_id for inst_id, _ in olt_cls.items()
412 if isinstance(inst_id, int) and inst_id in onu_cls}
413
414 for inst_id in inst_ids:
415 omci_attributes = {k for k in olt_cls[inst_id][ATTRIBUTES_KEY].iterkeys()}
416 onu_attributes = {k for k in onu_cls[inst_id][ATTRIBUTES_KEY].iterkeys()}
417
418 # Get attributes that exist in one database, but not the other
419 sym_diffs = (omci_attributes ^ onu_attributes) - ro_attrs
420 results.extend([(cls_id, inst_id, attr) for attr in sym_diffs])
421
422 # Get common attributes with different values
423 common_attributes = (omci_attributes & onu_attributes) - ro_attrs
424 results.extend([(cls_id, inst_id, attr) for attr in common_attributes
425 if olt_cls[inst_id][ATTRIBUTES_KEY][attr] !=
426 onu_cls[inst_id][ATTRIBUTES_KEY][attr]])
427 return results