blob: 2af69232d2234c3c58a6b8196c5b1182cc598608 [file] [log] [blame]
Chip Boling32aab302019-01-23 10:50:18 -06001#
2# Copyright 2018 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 mib_db_api import *
17from voltha.protos.omci_alarm_db_pb2 import AlarmInstanceData, AlarmClassData, \
18 AlarmDeviceData, AlarmAttributeData
19
20
21class AlarmDbExternal(MibDbApi):
22 """
23 A persistent external OpenOMCI Alarm Database
24 """
25 CURRENT_VERSION = 1 # VOLTHA v1.3.0 release
26 ALARM_BITMAP_KEY = 'alarm_bit_map'
27
28 _TIME_FORMAT = '%Y%m%d-%H%M%S.%f'
29
30 # Paths from root proxy
31 ALARM_PATH = '/omci_alarms'
32 DEVICE_PATH = ALARM_PATH + '/{}' # .format(device_id)
33
34 # Classes, Instances, and Attributes as lists from root proxy
35 CLASSES_PATH = DEVICE_PATH + '/classes' # .format(device_id)
36 INSTANCES_PATH = DEVICE_PATH + '/classes/{}/instances' # .format(device_id, class_id)
37 ATTRIBUTES_PATH = DEVICE_PATH + '/classes/{}/instances/{}/attributes' # .format(device_id, class_id, instance_id)
38
39 # Single Class, Instance, and Attribute as objects from device proxy
40 CLASS_PATH = '/classes/{}' # .format(class_id)
41 INSTANCE_PATH = '/classes/{}/instances/{}' # .format(class_id, instance_id)
42 ATTRIBUTE_PATH = '/classes/{}/instances/{}/attributes/{}' # .format(class_id, instance_id
43 # attribute_name)
44
45 def __init__(self, omci_agent):
46 """
47 Class initializer
48 :param omci_agent: (OpenOMCIAgent) OpenOMCI Agent
49 """
50 super(AlarmDbExternal, self).__init__(omci_agent)
51 self._core = omci_agent.core
52
53 def start(self):
54 """
55 Start up/restore the database
56 """
57 self.log.debug('start')
58
59 if not self._started:
60 super(AlarmDbExternal, self).start()
61 root_proxy = self._core.get_proxy('/')
62
63 try:
64 base = root_proxy.get(AlarmDbExternal.ALARM_PATH)
65 self.log.info('db-exists', num_devices=len(base))
66
67 except Exception as e:
68 self.log.exception('start-failure', e=e)
69 raise
70
71 def stop(self):
72 """
73 Start up the database
74 """
75 self.log.debug('stop')
76
77 if self._started:
78 super(AlarmDbExternal, self).stop()
79 # TODO: Delete this method if nothing else is done except calling the base class
80
81 def _time_to_string(self, time):
82 return time.strftime(AlarmDbExternal._TIME_FORMAT) if time is not None else ''
83
84 def _string_to_time(self, time):
85 return datetime.strptime(time, AlarmDbExternal._TIME_FORMAT) if len(time) else None
86
87 def _attribute_to_string(self, value):
88 """
89 Convert an ME's attribute value to string representation
90
91 :param value: (long) Alarm bitmaps are always a Long
92 :return: (str) String representation of the value
93 """
94 return str(value)
95
96 def _string_to_attribute(self, str_value):
97 """
98 Convert an ME's attribute value-string to its Scapy decode equivalent
99
100 :param device_id: (str) ONU Device ID
101 :param class_id: (int) Class ID
102 :param attr_name: (str) Attribute Name (see EntityClasses)
103 :param str_value: (str) Attribute Value in string form
104
105 :return: (various) String representation of the value
106 :raises KeyError: Device, Class ID, or Attribute does not exist
107 """
108 # Alarms are always a bitmap which is a long
109 return long(str_value) if len(str_value) else 0L
110
111 def add(self, device_id, overwrite=False):
112 """
113 Add a new ONU to database
114
115 :param device_id: (str) Device ID of ONU to add
116 :param overwrite: (bool) Overwrite existing entry if found.
117
118 :raises KeyError: If device already exists and 'overwrite' is False
119 """
120 self.log.debug('add-device', device_id=device_id, overwrite=overwrite)
121
122 now = datetime.utcnow()
123 found = False
124 root_proxy = self._core.get_proxy('/')
125
126 data = AlarmDeviceData(device_id=device_id,
127 created=self._time_to_string(now),
128 version=AlarmDbExternal.CURRENT_VERSION,
129 last_alarm_sequence=0)
130 try:
131 dev_proxy = self._device_proxy(device_id)
132 found = True
133
134 if not overwrite:
135 # Device already exists
136 raise KeyError('Device with ID {} already exists in Alarm database'.
137 format(device_id))
138
139 # Overwrite with new data
140 data = dev_proxy.get('/', depth=0)
141 self._root_proxy.update(AlarmDbExternal.DEVICE_PATH.format(device_id), data)
142 self._modified = now
143
144 except KeyError:
145 if found:
146 raise
147 # Did not exist, add it now
148 root_proxy.add(AlarmDbExternal.ALARM_PATH, data)
149 self._created = now
150 self._modified = now
151
152 def remove(self, device_id):
153 """
154 Remove an ONU from the database
155
156 :param device_id: (str) Device ID of ONU to remove from database
157 """
158 self.log.debug('remove-device', device_id=device_id)
159
160 if not self._started:
161 raise DatabaseStateError('The Database is not currently active')
162
163 if not isinstance(device_id, basestring):
164 raise TypeError('Device ID should be an string')
165
166 try:
167 # self._root_proxy.get(AlarmDbExternal.DEVICE_PATH.format(device_id))
168 self._root_proxy.remove(AlarmDbExternal.DEVICE_PATH.format(device_id))
169 self._modified = datetime.utcnow()
170
171 except KeyError:
172 # Did not exists, which is not a failure
173 pass
174
175 except Exception as e:
176 self.log.exception('remove-exception', device_id=device_id, e=e)
177 raise
178
179 @property
180 def _root_proxy(self):
181 return self._core.get_proxy('/')
182
183 def _device_proxy(self, device_id):
184 """
185 Return a config proxy to the OMCI Alarm_DB leaf for a given device
186
187 :param device_id: (str) ONU Device ID
188 :return: (ConfigProxy) Configuration proxy rooted at OMCI Alarm DB
189 :raises KeyError: If the device does not exist in the database
190 """
191 if not isinstance(device_id, basestring):
192 raise TypeError('Device ID should be an string')
193
194 if not self._started:
195 raise DatabaseStateError('The Database is not currently active')
196
197 return self._core.get_proxy(AlarmDbExternal.DEVICE_PATH.format(device_id))
198
199 def _class_proxy(self, device_id, class_id, create=False):
200 """
201 Get a config proxy to a specific managed entity class
202 :param device_id: (str) ONU Device ID
203 :param class_id: (int) Class ID
204 :param create: (bool) If true, create default instance (and class)
205 :return: (ConfigProxy) Class configuration proxy
206
207 :raises DatabaseStateError: If database is not started
208 :raises KeyError: If Instance does not exist and 'create' is False
209 """
210 if not self._started:
211 raise DatabaseStateError('The Database is not currently active')
212
213 if not 0 <= class_id <= 0xFFFF:
214 raise ValueError('class-id is 0..0xFFFF')
215
216 fmt = AlarmDbExternal.DEVICE_PATH + AlarmDbExternal.CLASS_PATH
217 path = fmt.format(device_id, class_id)
218
219 try:
220 return self._core.get_proxy(path)
221
222 except KeyError:
223 if not create:
224 self.log.error('class-proxy-does-not-exist', device_id=device_id,
225 class_id=class_id)
226 raise
227
228 # Create class
229 data = AlarmClassData(class_id=class_id)
230 root_path = AlarmDbExternal.CLASSES_PATH.format(device_id)
231 self._root_proxy.add(root_path, data)
232
233 return self._core.get_proxy(path)
234
235 def _instance_proxy(self, device_id, class_id, instance_id, create=False):
236 """
237 Get a config proxy to a specific managed entity instance
238 :param device_id: (str) ONU Device ID
239 :param class_id: (int) Class ID
240 :param instance_id: (int) Instance ID
241 :param create: (bool) If true, create default instance (and class)
242 :return: (ConfigProxy) Instance configuration proxy
243
244 :raises DatabaseStateError: If database is not started
245 :raises KeyError: If Instance does not exist and 'create' is False
246 """
247 if not self._started:
248 raise DatabaseStateError('The Database is not currently active')
249
250 if not isinstance(device_id, basestring):
251 raise TypeError('Device ID is a string')
252
253 if not 0 <= class_id <= 0xFFFF:
254 raise ValueError('class-id is 0..0xFFFF')
255
256 if not 0 <= instance_id <= 0xFFFF:
257 raise ValueError('instance-id is 0..0xFFFF')
258
259 fmt = AlarmDbExternal.DEVICE_PATH + AlarmDbExternal.INSTANCE_PATH
260 path = fmt.format(device_id, class_id, instance_id)
261
262 try:
263 return self._core.get_proxy(path)
264
265 except KeyError:
266 if not create:
267 self.log.error('instance-proxy-does-not-exist', device_id=device_id,
268 class_id=class_id, instance_id=instance_id)
269 raise
270
271 # Create instance, first make sure class exists
272 self._class_proxy(device_id, class_id, create=True)
273
274 now = self._time_to_string(datetime.utcnow())
275 data = AlarmInstanceData(instance_id=instance_id, created=now, modified=now)
276 root_path = AlarmDbExternal.INSTANCES_PATH.format(device_id, class_id)
277 self._root_proxy.add(root_path, data)
278
279 return self._core.get_proxy(path)
280
281 def save_last_sync_time(self, device_id, value):
282 """
283 Save the Last Sync time to the database in an easy location to access
284
285 :param device_id: (str) ONU Device ID
286 :param value: (DateTime) Value to save
287 """
288 self.log.debug('save-last-sync-time', device_id=device_id, time=str(value))
289
290 try:
291 if not isinstance(value, datetime):
292 raise TypeError('Expected a datetime object, got {}'.
293 format(type(datetime)))
294
295 device_proxy = self._device_proxy(device_id)
296 data = device_proxy.get(depth=0)
297
298 now = datetime.utcnow()
299 data.last_sync_time = self._time_to_string(value)
300
301 # Update
302 self._root_proxy.update(AlarmDbExternal.DEVICE_PATH.format(device_id),
303 data)
304 self._modified = now
305 self.log.debug('save-sync-time-complete', device_id=device_id)
306
307 except Exception as e:
308 self.log.exception('save-last-sync-exception', device_id=device_id, e=e)
309 raise
310
311 def get_last_sync_time(self, device_id):
312 """
313 Get the Last Sync Time saved to the database for a device
314
315 :param device_id: (str) ONU Device ID
316 :return: (int) The Value or None if not found
317 """
318 self.log.debug('get-last-sync-time', device_id=device_id)
319
320 try:
321 device_proxy = self._device_proxy(device_id)
322 data = device_proxy.get(depth=0)
323 return self._string_to_time(data.last_sync_time)
324
325 except KeyError:
326 return None # OMCI MIB_DB entry has not yet been created
327
328 except Exception as e:
329 self.log.exception('get-last-sync-time-exception', e=e)
330 raise
331
332 def save_alarm_last_sync(self, device_id, value):
333 """
334 Save the Last Alarm Sequence value to the database in an easy location to access
335
336 :param device_id: (str) ONU Device ID
337 :param value: (int) Value to save
338 """
339 self.log.debug('save-last-sync', device_id=device_id, seq=str(value))
340
341 try:
342 if not isinstance(value, int):
343 raise TypeError('Expected a integer, got {}'.format(type(value)))
344
345 device_proxy = self._device_proxy(device_id)
346 data = device_proxy.get(depth=0)
347
348 now = datetime.utcnow()
349 data.last_alarm_sequence = int(value)
350
351 # Update
352 self._root_proxy.update(AlarmDbExternal.DEVICE_PATH.format(device_id),
353 data)
354 self._modified = now
355 self.log.debug('save-sequence-complete', device_id=device_id)
356
357 except Exception as e:
358 self.log.exception('save-last-sync-exception', device_id=device_id, e=e)
359 raise
360
361 def get_alarm_last_sync(self, device_id):
362 """
363 Get the Last Sync Time saved to the database for a device
364
365 :param device_id: (str) ONU Device ID
366 :return: (int) The Value or None if not found
367 """
368 self.log.debug('get-last-sync', device_id=device_id)
369
370 try:
371 device_proxy = self._device_proxy(device_id)
372 data = device_proxy.get(depth=0)
373 return int(data.last_alarm_sequence)
374
375 except KeyError:
376 return None # OMCI ALARM_DB entry has not yet been created
377
378 except Exception as e:
379 self.log.exception('get-last-alarm-exception', e=e)
380 raise
381
382 def _add_new_class(self, device_id, class_id, instance_id, attributes):
383 """
384 Create an entry for a new class in the external database
385
386 :param device_id: (str) ONU Device ID
387 :param class_id: (int) ME Class ID
388 :param instance_id: (int) ME Entity ID
389 :param attributes: (dict) Attribute dictionary
390
391 :returns: (bool) True if the value was saved to the database. False if the
392 value was identical to the current instance
393 """
394 self.log.debug('add', device_id=device_id, class_id=class_id,
395 instance_id=instance_id, attributes=attributes)
396
397 now = self._time_to_string(datetime.utcnow())
398 attrs = [AlarmAttributeData(name=k,
399 value=self._attribute_to_string(v)) for k, v in attributes.items()]
400 class_data = AlarmClassData(class_id=class_id,
401 instances=[AlarmInstanceData(instance_id=instance_id,
402 created=now,
403 modified=now,
404 attributes=attrs)])
405
406 self._root_proxy.add(AlarmDbExternal.CLASSES_PATH.format(device_id), class_data)
407 self.log.debug('set-complete', device_id=device_id, class_id=class_id,
408 entity_id=instance_id, attributes=attributes)
409 return True
410
411 def _add_new_instance(self, device_id, class_id, instance_id, attributes):
412 """
413 Create an entry for a instance of an existing class in the external database
414
415 :param device_id: (str) ONU Device ID
416 :param class_id: (int) ME Class ID
417 :param instance_id: (int) ME Entity ID
418 :param attributes: (dict) Attribute dictionary
419
420 :returns: (bool) True if the value was saved to the database. False if the
421 value was identical to the current instance
422 """
423 self.log.debug('add', device_id=device_id, class_id=class_id,
424 instance_id=instance_id, attributes=attributes)
425
426 now = self._time_to_string(datetime.utcnow())
427 attrs = [AlarmAttributeData(name=k,
428 value=self._attribute_to_string(v)) for k, v in attributes.items()]
429 instance_data = AlarmInstanceData(instance_id=instance_id,
430 created=now,
431 modified=now,
432 attributes=attrs)
433
434 self._root_proxy.add(AlarmDbExternal.INSTANCES_PATH.format(device_id, class_id),
435 instance_data)
436
437 self.log.debug('set-complete', device_id=device_id, class_id=class_id,
438 entity_id=instance_id, attributes=attributes)
439 return True
440
441 def set(self, device_id, class_id, instance_id, attributes):
442 """
443 Set a database value. This should only be called by the Alarm synchronizer
444 and its related tasks
445
446 :param device_id: (str) ONU Device ID
447 :param class_id: (int) ME Class ID
448 :param instance_id: (int) ME Entity ID
449 :param attributes: (dict) Attribute dictionary
450
451 :returns: (bool) True if the value was saved to the database. False if the
452 value was identical to the current instance
453
454 :raises KeyError: If device does not exist
455 :raises DatabaseStateError: If the database is not enabled
456 """
457 self.log.debug('set', device_id=device_id, class_id=class_id,
458 instance_id=instance_id, attributes=attributes)
459 try:
460 if not isinstance(device_id, basestring):
461 raise TypeError('Device ID should be a string')
462
463 if not 0 <= class_id <= 0xFFFF:
464 raise ValueError("Invalid Class ID: {}, should be 0..65535".format(class_id))
465
466 if not 0 <= instance_id <= 0xFFFF:
467 raise ValueError("Invalid Instance ID: {}, should be 0..65535".format(instance_id))
468
469 if not isinstance(attributes, dict):
470 raise TypeError("Attributes should be a dictionary")
471
472 if not self._started:
473 raise DatabaseStateError('The Database is not currently active')
474
475 # Determine the best strategy to add the information
476 dev_proxy = self._device_proxy(device_id)
477
478 try:
479 class_data = dev_proxy.get(AlarmDbExternal.CLASS_PATH.format(class_id), deep=True)
480
481 inst_data = next((inst for inst in class_data.instances
482 if inst.instance_id == instance_id), None)
483
484 if inst_data is None:
485 return self._add_new_instance(device_id, class_id, instance_id, attributes)
486
487 # Possibly adding to or updating an existing instance
488 # Get instance proxy, creating it if needed
489
490 exist_attr_indexes = dict()
491 attr_len = len(inst_data.attributes)
492
493 for index in xrange(0, attr_len):
494 exist_attr_indexes[inst_data.attributes[index].name] = index
495
496 modified = False
497 str_value = ''
498 new_attributes = []
499
500 for k, v in attributes.items():
501 try:
502 str_value = self._attribute_to_string(v)
503 new_attributes.append(AlarmAttributeData(name=k, value=str_value))
504
505 except Exception as e:
506 self.log.exception('save-error', e=e, class_id=class_id,
507 attr=k, value_type=type(v))
508
509 if k not in exist_attr_indexes or \
510 inst_data.attributes[exist_attr_indexes[k]].value != str_value:
511 modified = True
512
513 if modified:
514 now = datetime.utcnow()
515 new_data = AlarmInstanceData(instance_id=instance_id,
516 created=inst_data.created,
517 modified=self._time_to_string(now),
518 attributes=new_attributes)
519 dev_proxy.remove(AlarmDbExternal.INSTANCE_PATH.format(class_id, instance_id))
520 self._root_proxy.add(AlarmDbExternal.INSTANCES_PATH.format(device_id,
521 class_id), new_data)
522
523 self.log.debug('set-complete', device_id=device_id, class_id=class_id,
524 entity_id=instance_id, attributes=attributes, modified=modified)
525 return modified
526
527 except KeyError:
528 # Here if the class-id does not yet exist in the database
529 return self._add_new_class(device_id, class_id, instance_id,
530 attributes)
531 except Exception as e:
532 self.log.exception('set-exception', device_id=device_id, class_id=class_id,
533 instance_id=instance_id, attributes=attributes, e=e)
534 raise
535
536 def delete(self, device_id, class_id, entity_id):
537 """
538 Delete an entity from the database if it exists. If all instances
539 of a class are deleted, the class is deleted as well.
540
541 :param device_id: (str) ONU Device ID
542 :param class_id: (int) ME Class ID
543 :param entity_id: (int) ME Entity ID
544
545 :returns: (bool) True if the instance was found and deleted. False
546 if it did not exist.
547
548 :raises KeyError: If device does not exist
549 :raises DatabaseStateError: If the database is not enabled
550 """
551 self.log.debug('delete', device_id=device_id, class_id=class_id,
552 entity_id=entity_id)
553
554 if not self._started:
555 raise DatabaseStateError('The Database is not currently active')
556
557 if not isinstance(device_id, basestring):
558 raise TypeError('Device ID should be an string')
559
560 if not 0 <= class_id <= 0xFFFF:
561 raise ValueError('class-id is 0..0xFFFF')
562
563 if not 0 <= entity_id <= 0xFFFF:
564 raise ValueError('instance-id is 0..0xFFFF')
565
566 try:
567 # Remove instance
568 self._instance_proxy(device_id, class_id, entity_id).remove('/')
569 now = datetime.utcnow()
570
571 # If resulting class has no instance, remove it as well
572 class_proxy = self._class_proxy(device_id, class_id)
573 class_data = class_proxy.get('/', depth=1)
574
575 if len(class_data.instances) == 0:
576 class_proxy.remove('/')
577
578 self._modified = now
579 return True
580
581 except KeyError:
582 return False # Not found
583
584 except Exception as e:
585 self.log.exception('get-last-data-exception', device_id=device_id, e=e)
586 raise
587
588 def query(self, device_id, class_id=None, instance_id=None, attributes=None):
589 """
590 Get database information.
591
592 This method can be used to request information from the database to the detailed
593 level requested
594
595 :param device_id: (str) ONU Device ID
596 :param class_id: (int) Managed Entity class ID
597 :param instance_id: (int) Managed Entity instance
598 :param attributes: (list/set or str) Managed Entity instance's attributes
599
600 :return: (dict) The value(s) requested. If class/inst/attribute is
601 not found, an empty dictionary is returned
602 :raises KeyError: If the requested device does not exist
603 :raises DatabaseStateError: If the database is not enabled
604 """
605 self.log.debug('query', device_id=device_id, class_id=class_id,
606 instance_id=instance_id, attributes=attributes)
607 try:
608 if class_id is None:
609 # Get full device info
610 dev_data = self._device_proxy(device_id).get('/', depth=-1)
611 data = self._device_to_dict(dev_data)
612
613 elif instance_id is None:
614 # Get all instances of the class
615 try:
616 cls_data = self._class_proxy(device_id, class_id).get('/', depth=-1)
617 data = self._class_to_dict(cls_data)
618
619 except KeyError:
620 data = dict()
621
622 else:
623 # Get all attributes of a specific ME
624 try:
625 inst_data = self._instance_proxy(device_id, class_id, instance_id).\
626 get('/', depth=-1)
627
628 if attributes is None:
629 # All Attributes
630 data = self._instance_to_dict(inst_data)
631
632 else:
633 # Specific attribute(s)
634 if isinstance(attributes, basestring):
635 attributes = {attributes}
636
637 data = {
638 attr.name: self._string_to_attribute(attr.value)
639 for attr in inst_data.attributes if attr.name in attributes}
640
641 except KeyError:
642 data = dict()
643
644 return data
645
646 except KeyError:
647 self.log.warn('query-no-device', device_id=device_id)
648 raise
649
650 except Exception as e:
651 self.log.exception('get-last-sync-exception', device_id=device_id, e=e)
652 raise
653
654 def _instance_to_dict(self, instance):
655 if not isinstance(instance, AlarmInstanceData):
656 raise TypeError('{} is not of type AlarmInstanceData'.format(type(instance)))
657
658 data = {
659 INSTANCE_ID_KEY: instance.instance_id,
660 CREATED_KEY: self._string_to_time(instance.created),
661 MODIFIED_KEY: self._string_to_time(instance.modified),
662 ATTRIBUTES_KEY: dict()
663 }
664 for attribute in instance.attributes:
665 data[ATTRIBUTES_KEY][attribute.name] = self._string_to_attribute(attribute.value)
666 return data
667
668 def _class_to_dict(self, val):
669 if not isinstance(val, AlarmClassData):
670 raise TypeError('{} is not of type AlarmClassData'.format(type(val)))
671
672 data = {
673 CLASS_ID_KEY: val.class_id,
674 }
675 for instance in val.instances:
676 data[instance.instance_id] = self._instance_to_dict(instance)
677 return data
678
679 def _device_to_dict(self, val):
680 if not isinstance(val, AlarmDeviceData):
681 raise TypeError('{} is not of type AlarmDeviceData'.format(type(val)))
682
683 data = {
684 DEVICE_ID_KEY: val.device_id,
685 CREATED_KEY: self._string_to_time(val.created),
686 VERSION_KEY: val.version,
687 ME_KEY: dict(),
688 MSG_TYPE_KEY: set()
689 }
690 for class_data in val.classes:
691 data[class_data.class_id] = self._class_to_dict(class_data)
692 for managed_entity in val.managed_entities:
693 data[ME_KEY][managed_entity.class_id] = managed_entity.name
694
695 for msg_type in val.message_types:
696 data[MSG_TYPE_KEY].add(msg_type.message_type)
697
698 return data