blob: d06a7fa3c5d2c4109b51871f4f6eb68a34afc6e7 [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_mib_db_pb2 import MibInstanceData, MibClassData, \
18 MibDeviceData, MibAttributeData, MessageType, ManagedEntity
19from voltha.extensions.omci.omci_entities import *
20from voltha.extensions.omci.omci_fields import *
21from scapy.fields import StrField, FieldListField, PacketField
22
23
24class MibDbStatistic(object):
25 """
26 For debug/tuning purposes.
27
28 With etcd around the Nov 8 time frame, (took out some created/modified settins) seeing the following:
29
30 o Creates: Avg: 141.4 mS, Min: 47 mS, Max: 323 mS (148 samples)
31 o Sets: Avg: 206.4 mS, Min: 85 mS, Max: 781 mS (142 samples)
32
33 With etcd around the Nov 7 time frame, seeing the following:
34
35 o Creates: Avg: 124.4 mS, Min: 48 mS, Max: 531 mS (148 samples)
36 o Sets: Avg: 210.7 mS, Min: 82 mS, Max: 944 mS (140 samples)
37 o Gets: Avg: 10.9 mS, Min: 0 mS, Max: 384 mS ( 99 samples)
38 o Deletes: No samples
39
40 With etcd around the v1.5 time frame, seeing the following:
41
42 o Creates: Avg: 57.1 mS, Min: 76 mS, Max: 511 mS (146 samples)
43 o Sets: Avg: 303.9 mS, Min: 126 mS, Max: 689 mS (103 samples)
44 o Gets: Avg: 3.3 mS, Min: 0 mS, Max: 8 mS ( 9 samples)
45 o Deletes: No samples
46 """
47 def __init__(self, name):
48 self._name = name
49 self._count = 0
50 self._total_time = 0 # Total milliseconds
51 self._min_time = 99999999
52 self._max_time = 0
53
54 def get_statistics(self):
55 return {
56 'name': self._name,
57 'count': self._count,
58 'total_time': self._total_time,
59 'min_time': self._min_time,
60 'max_time': self._max_time,
61 'avg_time': self._total_time / self._count if self._count > 0 else 0
62 }
63
64 def clear_statistics(self):
65 self._count = 0
66 self._total_time = 0 # Total milliseconds
67 self._min_time = 99999999
68 self._max_time = 0
69
70 def increment(self, time):
71 self._count += 1
72 self._total_time += time # Total milliseconds
73 if self._min_time > time:
74 self._min_time = time
75 if self._max_time < time:
76 self._max_time = time
77
78
79class MibDbExternal(MibDbApi):
80 """
81 A persistent external OpenOMCI MIB Database
82 """
83 CURRENT_VERSION = 1 # VOLTHA v1.3.0 release
84
85 _TIME_FORMAT = '%Y%m%d-%H%M%S.%f'
86
87 # Paths from root proxy
88 MIB_PATH = '/omci_mibs'
89 DEVICE_PATH = MIB_PATH + '/{}' # .format(device_id)
90
91 # Classes, Instances, and Attributes as lists from root proxy
92 CLASSES_PATH = DEVICE_PATH + '/classes' # .format(device_id)
93 INSTANCES_PATH = DEVICE_PATH + '/classes/{}/instances' # .format(device_id, class_id)
94 ATTRIBUTES_PATH = DEVICE_PATH + '/classes/{}/instances/{}/attributes' # .format(device_id, class_id, instance_id)
95
96 # Single Class, Instance, and Attribute as objects from device proxy
97 CLASS_PATH = '/classes/{}' # .format(class_id)
98 INSTANCE_PATH = '/classes/{}/instances/{}' # .format(class_id, instance_id)
99 ATTRIBUTE_PATH = '/classes/{}/instances/{}/attributes/{}' # .format(class_id, instance_id
100 # attribute_name)
101
102 def __init__(self, omci_agent):
103 """
104 Class initializer
105 :param omci_agent: (OpenOMCIAgent) OpenOMCI Agent
106 """
107 super(MibDbExternal, self).__init__(omci_agent)
108 self._core = omci_agent.core
109 # Some statistics to help with debug/tuning/...
110 self._statistics = {
111 'get': MibDbStatistic('get'),
112 'set': MibDbStatistic('set'),
113 'create': MibDbStatistic('create'),
114 'delete': MibDbStatistic('delete')
115 }
116
117 def start(self):
118 """
119 Start up/restore the database
120 """
121 self.log.debug('start')
122
123 if not self._started:
124 super(MibDbExternal, self).start()
125 root_proxy = self._core.get_proxy('/')
126
127 try:
128 base = root_proxy.get(MibDbExternal.MIB_PATH)
129 self.log.info('db-exists', num_devices=len(base))
130
131 except Exception as e:
132 self.log.exception('start-failure', e=e)
133 raise
134
135 def stop(self):
136 """
137 Start up the database
138 """
139 self.log.debug('stop')
140
141 if self._started:
142 super(MibDbExternal, self).stop()
143 # TODO: Delete this method if nothing else is done except calling the base class
144
145 def _time_to_string(self, time):
146 return time.strftime(MibDbExternal._TIME_FORMAT) if time is not None else ''
147
148 def _string_to_time(self, time):
149 return datetime.strptime(time, MibDbExternal._TIME_FORMAT) if len(time) else None
150
151 def _attribute_to_string(self, device_id, class_id, attr_name, value, old_value = None):
152 """
153 Convert an ME's attribute value to string representation
154
155 :param device_id: (str) ONU Device ID
156 :param class_id: (int) Class ID
157 :param attr_name: (str) Attribute Name (see EntityClasses)
158 :param value: (various) Attribute Value
159
160 :return: (str) String representation of the value
161 :raises KeyError: Device, Class ID, or Attribute does not exist
162 """
163 try:
164 me_map = self._omci_agent.get_device(device_id).me_map
165
166 if class_id in me_map:
167 entity = me_map[class_id]
168 attr_index = entity.attribute_name_to_index_map[attr_name]
169 eca = entity.attributes[attr_index]
170 field = eca.field
171 else:
172 # Here for auto-defined MEs (ones not defined in ME Map)
173 from voltha.extensions.omci.omci_cc import UNKNOWN_CLASS_ATTRIBUTE_KEY
174 field = StrFixedLenField(UNKNOWN_CLASS_ATTRIBUTE_KEY, None, 24)
175
176 if isinstance(field, StrFixedLenField):
177 from scapy.base_classes import Packet_metaclass
178 if hasattr(value, 'to_json') and not isinstance(value, basestring):
179 # Packet Class to string
180 str_value = value.to_json()
181 elif isinstance(field.default, Packet_metaclass) \
182 and hasattr(field.default, 'json_from_value'):
183 #and not isinstance(value, basestring):
184 # Value/hex of Packet Class to string
185 str_value = field.default.json_from_value(value)
186 else:
187 str_value = str(value)
188
189 elif isinstance(field, (StrField, MACField, IPField)):
190 # For StrField, value is an str already
191 # For MACField, value is a string in ':' delimited form
192 # For IPField, value is a string in '.' delimited form
193 str_value = str(value)
194
195 elif isinstance(field, (ByteField, ShortField, IntField, LongField)):
196 # For ByteField, ShortField, IntField, and LongField value is an int
197 str_value = str(value)
198
199 elif isinstance(field, BitField):
200 # For BitField, value is a long
201 #
202 str_value = str(value)
203
204 elif hasattr(field, 'to_json'):
205 str_value = field.to_json(value, old_value)
206
207 elif isinstance(field, FieldListField):
208 str_value = json.dumps(value, separators=(',', ':'))
209
210 else:
211 self.log.warning('default-conversion', type=type(field),
212 class_id=class_id, attribute=attr_name, value=str(value))
213 str_value = str(value)
214
215 return str_value
216
217 except Exception as e:
218 self.log.exception('attr-to-string', device_id=device_id,
219 class_id=class_id, attr=attr_name,
220 value=value, e=e)
221 raise
222
223 def _string_to_attribute(self, device_id, class_id, attr_name, str_value):
224 """
225 Convert an ME's attribute value-string to its Scapy decode equivalent
226
227 :param device_id: (str) ONU Device ID
228 :param class_id: (int) Class ID
229 :param attr_name: (str) Attribute Name (see EntityClasses)
230 :param str_value: (str) Attribute Value in string form
231
232 :return: (various) String representation of the value
233 :raises KeyError: Device, Class ID, or Attribute does not exist
234 """
235 try:
236 me_map = self._omci_agent.get_device(device_id).me_map
237
238 if class_id in me_map:
239 entity = me_map[class_id]
240 attr_index = entity.attribute_name_to_index_map[attr_name]
241 eca = entity.attributes[attr_index]
242 field = eca.field
243 else:
244 # Here for auto-defined MEs (ones not defined in ME Map)
245 from voltha.extensions.omci.omci_cc import UNKNOWN_CLASS_ATTRIBUTE_KEY
246 field = StrFixedLenField(UNKNOWN_CLASS_ATTRIBUTE_KEY, None, 24)
247
248 if isinstance(field, StrFixedLenField):
249 from scapy.base_classes import Packet_metaclass
250 default = field.default
251 if isinstance(default, Packet_metaclass) and \
252 hasattr(default, 'to_json'):
253 value = json.loads(str_value)
254 else:
255 value = str_value
256
257 elif isinstance(field, MACField):
258 value = str_value
259
260 elif isinstance(field, IPField):
261 value = str_value
262
263 elif isinstance(field, (ByteField, ShortField, IntField, LongField)):
264 if str_value.lower() in ('true', 'false'):
265 str_value = '1' if str_value.lower() == 'true' else '0'
266 value = int(str_value)
267
268 elif isinstance(field, BitField):
269 value = long(str_value)
270
271 elif hasattr(field, 'load_json'):
272 value = field.load_json(str_value)
273
274 elif isinstance(field, FieldListField):
275 value = json.loads(str_value)
276
277 else:
278 self.log.warning('default-conversion', type=type(field),
279 class_id=class_id, attribute=attr_name, value=str_value)
280 value = None
281
282 return value
283
284 except Exception as e:
285 self.log.exception('attr-to-string', device_id=device_id,
286 class_id=class_id, attr=attr_name,
287 value=str_value, e=e)
288 raise
289
290 def add(self, device_id, overwrite=False):
291 """
292 Add a new ONU to database
293
294 :param device_id: (str) Device ID of ONU to add
295 :param overwrite: (bool) Overwrite existing entry if found.
296
297 :raises KeyError: If device already exists and 'overwrite' is False
298 """
299 self.log.debug('add-device', device_id=device_id, overwrite=overwrite)
300
301 now = datetime.utcnow()
302 found = False
303 root_proxy = self._core.get_proxy('/')
304
305 data = MibDeviceData(device_id=device_id,
306 created=self._time_to_string(now),
307 last_sync_time='',
308 mib_data_sync=0,
309 version=MibDbExternal.CURRENT_VERSION)
310 try:
311 dev_proxy = self._device_proxy(device_id)
312 found = True
313
314 if not overwrite:
315 # Device already exists
316 raise KeyError('Device with ID {} already exists in MIB database'.
317 format(device_id))
318
319 # Overwrite with new data
320 data = dev_proxy.get('/', depth=0)
321 self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id), data)
322 self._modified = now
323
324 except KeyError:
325 if found:
326 raise
327 # Did not exist, add it now
328 root_proxy.add(MibDbExternal.MIB_PATH, data)
329 self._created = now
330 self._modified = now
331
332 def remove(self, device_id):
333 """
334 Remove an ONU from the database
335
336 :param device_id: (str) Device ID of ONU to remove from database
337 """
338 self.log.debug('remove-device', device_id=device_id)
339
340 if not self._started:
341 raise DatabaseStateError('The Database is not currently active')
342
343 if not isinstance(device_id, basestring):
344 raise TypeError('Device ID should be an string')
345
346 try:
347 # self._root_proxy.get(MibDbExternal.DEVICE_PATH.format(device_id))
348 self._root_proxy.remove(MibDbExternal.DEVICE_PATH.format(device_id))
349 self._modified = datetime.utcnow()
350
351 except KeyError:
352 # Did not exists, which is not a failure
353 pass
354
355 except Exception as e:
356 self.log.exception('remove-exception', device_id=device_id, e=e)
357 raise
358
359 @property
360 def _root_proxy(self):
361 return self._core.get_proxy('/')
362
363 def _device_proxy(self, device_id):
364 """
365 Return a config proxy to the OMCI MIB_DB leaf for a given device
366
367 :param device_id: (str) ONU Device ID
368 :return: (ConfigProxy) Configuration proxy rooted at OMCI MIB DB
369 :raises KeyError: If the device does not exist in the database
370 """
371 if not isinstance(device_id, basestring):
372 raise TypeError('Device ID should be an string')
373
374 if not self._started:
375 raise DatabaseStateError('The Database is not currently active')
376
377 return self._core.get_proxy(MibDbExternal.DEVICE_PATH.format(device_id))
378
379 def _class_proxy(self, device_id, class_id, create=False):
380 """
381 Get a config proxy to a specific managed entity class
382 :param device_id: (str) ONU Device ID
383 :param class_id: (int) Class ID
384 :param create: (bool) If true, create default instance (and class)
385 :return: (ConfigProxy) Class configuration proxy
386
387 :raises DatabaseStateError: If database is not started
388 :raises KeyError: If Instance does not exist and 'create' is False
389 """
390 if not self._started:
391 raise DatabaseStateError('The Database is not currently active')
392
393 if not 0 <= class_id <= 0xFFFF:
394 raise ValueError('class-id is 0..0xFFFF')
395
396 fmt = MibDbExternal.DEVICE_PATH + MibDbExternal.CLASS_PATH
397 path = fmt.format(device_id, class_id)
398
399 try:
400 return self._core.get_proxy(path)
401
402 except KeyError:
403 if not create:
404 # This can occur right after a MIB Reset if the ONU publishes AVCs right away
405 # and during the MIB audit resync for ONU created MEs in response to an OLT
406 # created ME. Fail since for these test cases they occur during a verification
407 # 'query' and not the ME creation during resync. Calling code should handle
408 # they exception if it is expected to occur on occasion.
409 self.log.debug('class-proxy-does-not-exist', device_id=device_id,
410 class_id=class_id)
411 raise
412
413 # Create class
414 data = MibClassData(class_id=class_id)
415 root_path = MibDbExternal.CLASSES_PATH.format(device_id)
416 self._root_proxy.add(root_path, data)
417
418 return self._core.get_proxy(path)
419
420 def _instance_proxy(self, device_id, class_id, instance_id, create=False):
421 """
422 Get a config proxy to a specific managed entity instance
423 :param device_id: (str) ONU Device ID
424 :param class_id: (int) Class ID
425 :param instance_id: (int) Instance ID
426 :param create: (bool) If true, create default instance (and class)
427 :return: (ConfigProxy) Instance configuration proxy
428
429 :raises DatabaseStateError: If database is not started
430 :raises KeyError: If Instance does not exist and 'create' is False
431 """
432 if not self._started:
433 raise DatabaseStateError('The Database is not currently active')
434
435 if not isinstance(device_id, basestring):
436 raise TypeError('Device ID is a string')
437
438 if not 0 <= class_id <= 0xFFFF:
439 raise ValueError('class-id is 0..0xFFFF')
440
441 if not 0 <= instance_id <= 0xFFFF:
442 raise ValueError('instance-id is 0..0xFFFF')
443
444 fmt = MibDbExternal.DEVICE_PATH + MibDbExternal.INSTANCE_PATH
445 path = fmt.format(device_id, class_id, instance_id)
446
447 try:
448 return self._core.get_proxy(path)
449
450 except KeyError:
451 if not create:
452 # This can occur right after a MIB Reset if the ONU publishes AVCs right away
453 # and during the MIB audit resync for ONU created MEs in response to an OLT
454 # created ME. Fail since for these test cases they occur during a verification
455 # 'query' and not the ME creation during resync. Calling code should handle
456 # they exception if it is expected to occur on occasion.
457 self.log.info('instance-proxy-does-not-exist', device_id=device_id,
458 class_id=class_id, instance_id=instance_id)
459 raise
460
461 # Create instance, first make sure class exists
462 self._class_proxy(device_id, class_id, create=True)
463
464 now = self._time_to_string(datetime.utcnow())
465 data = MibInstanceData(instance_id=instance_id, created=now, modified=now)
466 root_path = MibDbExternal.INSTANCES_PATH.format(device_id, class_id)
467 self._root_proxy.add(root_path, data)
468
469 return self._core.get_proxy(path)
470
471 def on_mib_reset(self, device_id):
472 """
473 Reset/clear the database for a specific Device
474
475 :param device_id: (str) ONU Device ID
476 :raises DatabaseStateError: If the database is not enabled
477 :raises KeyError: If the device does not exist in the database
478 """
479 self.log.debug('on-mib-reset', device_id=device_id)
480
481 try:
482 device_proxy = self._device_proxy(device_id)
483 data = device_proxy.get(depth=2)
484
485 # Wipe out any existing class IDs
486 class_ids = [c.class_id for c in data.classes]
487
488 if len(class_ids):
489 for class_id in class_ids:
490 device_proxy.remove(MibDbExternal.CLASS_PATH.format(class_id))
491
492 # Reset MIB Data Sync to zero
493 now = datetime.utcnow()
494 data = MibDeviceData(device_id=device_id,
495 created=data.created,
496 last_sync_time=data.last_sync_time,
497 mib_data_sync=0,
498 version=MibDbExternal.CURRENT_VERSION)
499 # Update
500 self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
501 data)
502 self._modified = now
503 self.log.debug('mib-reset-complete', device_id=device_id)
504
505 except Exception as e:
506 self.log.exception('mib-reset-exception', device_id=device_id, e=e)
507 raise
508
509 def save_mib_data_sync(self, device_id, value):
510 """
511 Save the MIB Data Sync to the database in an easy location to access
512
513 :param device_id: (str) ONU Device ID
514 :param value: (int) Value to save
515 """
516 self.log.debug('save-mds', device_id=device_id, value=value)
517
518 try:
519 if not isinstance(value, int):
520 raise TypeError('MIB Data Sync is an integer')
521
522 if not 0 <= value <= 255:
523 raise ValueError('Invalid MIB-data-sync value {}. Must be 0..255'.
524 format(value))
525 device_proxy = self._device_proxy(device_id)
526 data = device_proxy.get(depth=0)
527
528 now = datetime.utcnow()
529 data.mib_data_sync = value
530
531 # Update
532 self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
533 data)
534 self._modified = now
535 self.log.debug('save-mds-complete', device_id=device_id)
536
537 except Exception as e:
538 self.log.exception('save-mds-exception', device_id=device_id, e=e)
539 raise
540
541 def get_mib_data_sync(self, device_id):
542 """
543 Get the MIB Data Sync value last saved to the database for a device
544
545 :param device_id: (str) ONU Device ID
546 :return: (int) The Value or None if not found
547 """
548 self.log.debug('get-mds', device_id=device_id)
549
550 try:
551 device_proxy = self._device_proxy(device_id)
552 data = device_proxy.get(depth=0)
553 return int(data.mib_data_sync)
554
555 except KeyError:
556 return None # OMCI MIB_DB entry has not yet been created
557
558 except Exception as e:
559 self.log.exception('get-mds-exception', device_id=device_id, e=e)
560 raise
561
562 def save_last_sync(self, device_id, value):
563 """
564 Save the Last Sync time to the database in an easy location to access
565
566 :param device_id: (str) ONU Device ID
567 :param value: (DateTime) Value to save
568 """
569 self.log.debug('save-last-sync', device_id=device_id, time=str(value))
570
571 try:
572 if not isinstance(value, datetime):
573 raise TypeError('Expected a datetime object, got {}'.
574 format(type(datetime)))
575
576 device_proxy = self._device_proxy(device_id)
577 data = device_proxy.get(depth=0)
578
579 now = datetime.utcnow()
580 data.last_sync_time = self._time_to_string(value)
581
582 # Update
583 self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
584 data)
585 self._modified = now
586 self.log.debug('save-mds-complete', device_id=device_id)
587
588 except Exception as e:
589 self.log.exception('save-last-sync-exception', device_id=device_id, e=e)
590 raise
591
592 def get_last_sync(self, device_id):
593 """
594 Get the Last Sync Time saved to the database for a device
595
596 :param device_id: (str) ONU Device ID
597 :return: (int) The Value or None if not found
598 """
599 self.log.debug('get-last-sync', device_id=device_id)
600
601 try:
602 device_proxy = self._device_proxy(device_id)
603 data = device_proxy.get(depth=0)
604 return self._string_to_time(data.last_sync_time)
605
606 except KeyError:
607 return None # OMCI MIB_DB entry has not yet been created
608
609 except Exception as e:
610 self.log.exception('get-last-sync-exception', e=e)
611 raise
612
613 def _add_new_class(self, device_id, class_id, instance_id, attributes):
614 """
615 Create an entry for a new class in the external database
616
617 :param device_id: (str) ONU Device ID
618 :param class_id: (int) ME Class ID
619 :param instance_id: (int) ME Entity ID
620 :param attributes: (dict) Attribute dictionary
621
622 :returns: (bool) True if the value was saved to the database. False if the
623 value was identical to the current instance
624 """
625 self.log.debug('add', device_id=device_id, class_id=class_id,
626 instance_id=instance_id, attributes=attributes)
627
628 now = self._time_to_string(datetime.utcnow())
629 attrs = []
630 for k, v in attributes.items():
631 if k == 'serial_number':
632 vendor_id = str(v[0:4])
633 vendor_specific = v[4:]
634 vendor_specific = str(vendor_specific.encode('hex'))
635 str_value = vendor_id + vendor_specific
636 attrs.append(MibAttributeData(name=k, value=str_value))
637 else:
638 str_value = self._attribute_to_string(device_id, class_id, k, v)
639 attrs.append(MibAttributeData(name=k, value=str_value))
640
641 class_data = MibClassData(class_id=class_id,
642 instances=[MibInstanceData(instance_id=instance_id,
643 created=now,
644 modified=now,
645 attributes=attrs)])
646
647 self._root_proxy.add(MibDbExternal.CLASSES_PATH.format(device_id), class_data)
648 self.log.debug('set-complete', device_id=device_id, class_id=class_id,
649 entity_id=instance_id, attributes=attributes)
650 return True
651
652 def _add_new_instance(self, device_id, class_id, instance_id, attributes):
653 """
654 Create an entry for a instance of an existing class in the external database
655
656 :param device_id: (str) ONU Device ID
657 :param class_id: (int) ME Class ID
658 :param instance_id: (int) ME Entity ID
659 :param attributes: (dict) Attribute dictionary
660
661 :returns: (bool) True if the value was saved to the database. False if the
662 value was identical to the current instance
663 """
664 self.log.debug('add', device_id=device_id, class_id=class_id,
665 instance_id=instance_id, attributes=attributes)
666
667 now = self._time_to_string(datetime.utcnow())
668 attrs = []
669 for k, v in attributes.items():
670 if k == 'serial_number':
671 vendor_id = str(v[0:4])
672 vendor_specific = v[4:]
673 vendor_specific = str(vendor_specific.encode('hex'))
674 str_value = vendor_id+vendor_specific
675 attrs.append(MibAttributeData(name=k, value=str_value))
676 else:
677 str_value = self._attribute_to_string(device_id, class_id, k, v)
678 attrs.append(MibAttributeData(name=k, value=str_value))
679
680 instance_data = MibInstanceData(instance_id=instance_id,
681 created=now,
682 modified=now,
683 attributes=attrs)
684
685 self._root_proxy.add(MibDbExternal.INSTANCES_PATH.format(device_id, class_id),
686 instance_data)
687
688 self.log.debug('set-complete', device_id=device_id, class_id=class_id,
689 entity_id=instance_id, attributes=attributes)
690 return True
691
692 def set(self, device_id, class_id, instance_id, attributes):
693 """
694 Set a database value. This should only be called by the MIB synchronizer
695 and its related tasks
696
697 :param device_id: (str) ONU Device ID
698 :param class_id: (int) ME Class ID
699 :param instance_id: (int) ME Entity ID
700 :param attributes: (dict) Attribute dictionary
701
702 :returns: (bool) True if the value was saved to the database. False if the
703 value was identical to the current instance
704
705 :raises KeyError: If device does not exist
706 :raises DatabaseStateError: If the database is not enabled
707 """
708 self.log.debug('set', device_id=device_id, class_id=class_id,
709 instance_id=instance_id, attributes=attributes)
710 try:
711 if not isinstance(device_id, basestring):
712 raise TypeError('Device ID should be a string')
713
714 if not 0 <= class_id <= 0xFFFF:
715 raise ValueError("Invalid Class ID: {}, should be 0..65535".format(class_id))
716
717 if not 0 <= instance_id <= 0xFFFF:
718 raise ValueError("Invalid Instance ID: {}, should be 0..65535".format(instance_id))
719
720 if not isinstance(attributes, dict):
721 raise TypeError("Attributes should be a dictionary")
722
723 if not self._started:
724 raise DatabaseStateError('The Database is not currently active')
725
726 # Determine the best strategy to add the information
727 dev_proxy = self._device_proxy(device_id)
728
729 operation = 'set'
730 start_time = None
731 try:
732 class_data = dev_proxy.get(MibDbExternal.CLASS_PATH.format(class_id), deep=True)
733
734 inst_data = next((inst for inst in class_data.instances
735 if inst.instance_id == instance_id), None)
736
737 if inst_data is None:
738 operation = 'create'
739 start_time = datetime.utcnow()
740 return self._add_new_instance(device_id, class_id, instance_id, attributes)
741
742 # Possibly adding to or updating an existing instance
743 # Get instance proxy, creating it if needed
744
745 modified = False
746 new_attributes = []
747 exist_attr_indexes = dict()
748 attr_len = len(inst_data.attributes)
749
750 for index in xrange(0, attr_len):
751 name = inst_data.attributes[index].name
752 value = inst_data.attributes[index].value
753 exist_attr_indexes[name] = index
754 new_attributes.append(MibAttributeData(name=name, value=value))
755
756 for k, v in attributes.items():
757 try:
758 old_value = None if k not in exist_attr_indexes \
759 else new_attributes[exist_attr_indexes[k]].value
760
761 str_value = self._attribute_to_string(device_id, class_id, k, v, old_value)
762
763 if k not in exist_attr_indexes:
764 new_attributes.append(MibAttributeData(name=k, value=str_value))
765 modified = True
766
767 elif new_attributes[exist_attr_indexes[k]].value != str_value:
768 new_attributes[exist_attr_indexes[k]].value = str_value
769 modified = True
770
771 except Exception as e:
772 self.log.exception('save-error', e=e, class_id=class_id,
773 attr=k, value_type=type(v))
774
775 if modified:
776 now = datetime.utcnow()
777 start_time = now
778 new_data = MibInstanceData(instance_id=instance_id,
779 created=inst_data.created,
780 modified=self._time_to_string(now),
781 attributes=new_attributes)
782 dev_proxy.remove(MibDbExternal.INSTANCE_PATH.format(class_id, instance_id))
783 self._root_proxy.add(MibDbExternal.INSTANCES_PATH.format(device_id,
784 class_id), new_data)
785 return modified
786
787 except KeyError:
788 # Here if the class-id does not yet exist in the database
789 self.log.debug("adding-key-not-found", class_id=class_id)
790 return self._add_new_class(device_id, class_id, instance_id,
791 attributes)
792 finally:
793 if start_time is not None:
794 diff = datetime.utcnow() - start_time
795 # NOTE: Change to 'debug' when checked in, manually change to 'info'
796 # for development testing.
797 self.log.debug('db-{}-time'.format(operation), milliseconds=diff.microseconds/1000)
798 self._statistics[operation].increment(diff.microseconds/1000)
799
800 except Exception as e:
801 self.log.exception('set-exception', device_id=device_id, class_id=class_id,
802 instance_id=instance_id, attributes=attributes, e=e)
803 raise
804
805 def delete(self, device_id, class_id, entity_id):
806 """
807 Delete an entity from the database if it exists. If all instances
808 of a class are deleted, the class is deleted as well.
809
810 :param device_id: (str) ONU Device ID
811 :param class_id: (int) ME Class ID
812 :param entity_id: (int) ME Entity ID
813
814 :returns: (bool) True if the instance was found and deleted. False
815 if it did not exist.
816
817 :raises KeyError: If device does not exist
818 :raises DatabaseStateError: If the database is not enabled
819 """
820 self.log.debug('delete', device_id=device_id, class_id=class_id,
821 entity_id=entity_id)
822
823 if not self._started:
824 raise DatabaseStateError('The Database is not currently active')
825
826 if not isinstance(device_id, basestring):
827 raise TypeError('Device ID should be an string')
828
829 if not 0 <= class_id <= 0xFFFF:
830 raise ValueError('class-id is 0..0xFFFF')
831
832 if not 0 <= entity_id <= 0xFFFF:
833 raise ValueError('instance-id is 0..0xFFFF')
834
835 start_time = datetime.utcnow()
836 try:
837 # Remove instance
838 self._instance_proxy(device_id, class_id, entity_id).remove('/')
839 now = datetime.utcnow()
840
841 # If resulting class has no instance, remove it as well
842 class_proxy = self._class_proxy(device_id, class_id)
843 class_data = class_proxy.get('/', depth=1)
844
845 if len(class_data.instances) == 0:
846 class_proxy.remove('/')
847
848 self._modified = now
849 return True
850
851 except KeyError:
852 return False # Not found
853
854 except Exception as e:
855 self.log.exception('get-last-sync-exception', device_id=device_id, e=e)
856 raise
857
858 finally:
859 diff = datetime.utcnow() - start_time
860 # NOTE: Change to 'debug' when checked in, manually change to 'info'
861 # for development testing.
862 self.log.debug('db-delete-time', milliseconds=diff.microseconds/1000)
863 self._statistics['delete'].increment(diff.microseconds/1000)
864
865 def query(self, device_id, class_id=None, instance_id=None, attributes=None):
866 """
867 Get database information.
868
869 This method can be used to request information from the database to the detailed
870 level requested
871
872 :param device_id: (str) ONU Device ID
873 :param class_id: (int) Managed Entity class ID
874 :param instance_id: (int) Managed Entity instance
875 :param attributes: (list/set or str) Managed Entity instance's attributes
876
877 :return: (dict) The value(s) requested. If class/inst/attribute is
878 not found, an empty dictionary is returned
879 :raises KeyError: If the requested device does not exist
880 :raises DatabaseStateError: If the database is not enabled
881 """
882 self.log.debug('query', device_id=device_id, class_id=class_id,
883 instance_id=instance_id, attributes=attributes)
884
885 start_time = datetime.utcnow()
886 end_time = None
887 try:
888 if class_id is None:
889 # Get full device info
890 dev_data = self._device_proxy(device_id).get('/', depth=-1)
891 end_time = datetime.utcnow()
892 data = self._device_to_dict(dev_data)
893
894 elif instance_id is None:
895 # Get all instances of the class
896 try:
897 cls_data = self._class_proxy(device_id, class_id).get('/', depth=-1)
898 end_time = datetime.utcnow()
899 data = self._class_to_dict(device_id, cls_data)
900
901 except KeyError:
902 data = dict()
903
904 else:
905 # Get all attributes of a specific ME
906 try:
907 inst_data = self._instance_proxy(device_id, class_id, instance_id).\
908 get('/', depth=-1)
909 end_time = datetime.utcnow()
910
911 if attributes is None:
912 # All Attributes
913 data = self._instance_to_dict(device_id, class_id, inst_data)
914
915 else:
916 # Specific attribute(s)
917 if isinstance(attributes, basestring):
918 attributes = {attributes}
919
920 data = {
921 attr.name: self._string_to_attribute(device_id,
922 class_id,
923 attr.name,
924 attr.value)
925 for attr in inst_data.attributes if attr.name in attributes}
926
927 except KeyError:
928 data = dict()
929
930 return data
931
932 except KeyError:
933 self.log.warn('query-no-device', device_id=device_id)
934 raise
935
936 except Exception as e:
937 self.log.exception('get-last-sync-exception', device_id=device_id, e=e)
938 raise
939
940 finally:
941 if end_time is not None:
942 diff = end_time.utcnow() - start_time
943 # NOTE: Change to 'debug' when checked in, manually change to 'info'
944 # for development testing.
945 self.log.debug('db-get-time', milliseconds=diff.microseconds/1000, class_id=class_id,
946 instance_id=instance_id)
947 self._statistics['get'].increment(diff.microseconds/1000)
948
949 def _instance_to_dict(self, device_id, class_id, instance):
950 if not isinstance(instance, MibInstanceData):
951 raise TypeError('{} is not of type MibInstanceData'.format(type(instance)))
952
953 data = {
954 INSTANCE_ID_KEY: instance.instance_id,
955 CREATED_KEY: self._string_to_time(instance.created),
956 MODIFIED_KEY: self._string_to_time(instance.modified),
957 ATTRIBUTES_KEY: dict()
958 }
959 for attribute in instance.attributes:
960 data[ATTRIBUTES_KEY][attribute.name] = self._string_to_attribute(device_id,
961 class_id,
962 attribute.name,
963 attribute.value)
964 return data
965
966 def _class_to_dict(self, device_id, val):
967 if not isinstance(val, MibClassData):
968 raise TypeError('{} is not of type MibClassData'.format(type(val)))
969
970 data = {
971 CLASS_ID_KEY: val.class_id,
972 }
973 for instance in val.instances:
974 data[instance.instance_id] = self._instance_to_dict(device_id,
975 val.class_id,
976 instance)
977 return data
978
979 def _device_to_dict(self, val):
980 if not isinstance(val, MibDeviceData):
981 raise TypeError('{} is not of type MibDeviceData'.format(type(val)))
982
983 data = {
984 DEVICE_ID_KEY: val.device_id,
985 CREATED_KEY: self._string_to_time(val.created),
986 LAST_SYNC_KEY: self._string_to_time(val.last_sync_time),
987 MDS_KEY: val.mib_data_sync,
988 VERSION_KEY: val.version,
989 ME_KEY: dict(),
990 MSG_TYPE_KEY: set()
991 }
992 for class_data in val.classes:
993 data[class_data.class_id] = self._class_to_dict(val.device_id,
994 class_data)
995 for managed_entity in val.managed_entities:
996 data[ME_KEY][managed_entity.class_id] = managed_entity.name
997
998 for msg_type in val.message_types:
999 data[MSG_TYPE_KEY].add(msg_type.message_type)
1000
1001 return data
1002
1003 def _managed_entity_to_name(self, device_id, class_id):
1004 me_map = self._omci_agent.get_device(device_id).me_map
1005 entity = me_map.get(class_id)
1006
1007 return entity.__name__ if entity is not None else 'UnknownManagedEntity'
1008
1009 def update_supported_managed_entities(self, device_id, managed_entities):
1010 """
1011 Update the supported OMCI Managed Entities for this device
1012 :param device_id: (str) ONU Device ID
1013 :param managed_entities: (set) Managed Entity class IDs
1014 """
1015 try:
1016 me_list = [ManagedEntity(class_id=class_id,
1017 name=self._managed_entity_to_name(device_id,
1018 class_id))
1019 for class_id in managed_entities]
1020
1021 device_proxy = self._device_proxy(device_id)
1022 data = device_proxy.get(depth=0)
1023
1024 now = datetime.utcnow()
1025 data.managed_entities.extend(me_list)
1026
1027 # Update
1028 self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
1029 data)
1030 self._modified = now
1031 self.log.debug('save-me-list-complete', device_id=device_id)
1032
1033 except Exception as e:
1034 self.log.exception('add-me-failure', e=e, me_list=managed_entities)
1035 raise
1036
1037 def update_supported_message_types(self, device_id, msg_types):
1038 """
1039 Update the supported OMCI Managed Entities for this device
1040 :param device_id: (str) ONU Device ID
1041 :param msg_types: (set) Message Type values (ints)
1042 """
1043 try:
1044 msg_type_list = [MessageType(message_type=msg_type.value)
1045 for msg_type in msg_types]
1046
1047 device_proxy = self._device_proxy(device_id)
1048 data = device_proxy.get(depth=0)
1049
1050 now = datetime.utcnow()
1051 data.message_types.extend(msg_type_list)
1052
1053 # Update
1054 self._root_proxy.update(MibDbExternal.DEVICE_PATH.format(device_id),
1055 data)
1056 self._modified = now
1057 self.log.debug('save-msg-types-complete', device_id=device_id)
1058
1059 except Exception as e:
1060 self.log.exception('add-msg-types-failure', e=e, msg_types=msg_types)
1061 raise