blob: 080b8494c0d1f0d485d9f4b3fa7edf2fd7ce80ed [file] [log] [blame]
Chip Boling67b674a2019-02-08 11:42: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#
Zack Williams84a71e92019-11-15 09:00:19 -070016from __future__ import absolute_import
Chip Boling67b674a2019-02-08 11:42:18 -060017import copy
Zack Williams84a71e92019-11-15 09:00:19 -070018from .mib_db_api import *
Chip Boling67b674a2019-02-08 11:42:18 -060019import json
Zack Williams84a71e92019-11-15 09:00:19 -070020import six
Chip Boling67b674a2019-02-08 11:42:18 -060021
22
23class MibDbVolatileDict(MibDbApi):
24 """
25 A very simple in-memory database for ME storage. Data is not persistent
26 across reboots.
27
28 In Phase 2, this DB will be instantiated on a per-ONU basis but act as if
29 it is shared for all ONUs. This class will be updated with and external
30 key-value store (or other appropriate database) in Voltha 1.3 Sprint 3
31
32 This class can be used for unit tests
33 """
34 CURRENT_VERSION = 1
35
36 def __init__(self, omci_agent):
37 """
38 Class initializer
39 :param omci_agent: (OpenOMCIAgent) OpenOMCI Agent
40 """
41 super(MibDbVolatileDict, self).__init__(omci_agent)
42 self._data = dict() # device_id -> ME ID -> Inst ID -> Attr Name -> Values
43
44 def start(self):
45 """
46 Start up/restore the database. For in-memory, will be a nop. For external
47 DB, may need to create the DB and fetch create/modified values
48 """
49 super(MibDbVolatileDict, self).start()
50 # TODO: Delete this method if nothing else is done except calling the base class
51
52 def stop(self):
53 """
54 Start up the database. For in-memory, will be a nop. For external
55 DB, may need to create the DB and fetch create/modified values
56 """
57 super(MibDbVolatileDict, self).stop()
58 # TODO: Delete this method if nothing else is done except calling the base class
59
60 def add(self, device_id, overwrite=False):
61 """
62 Add a new ONU to database
63
64 :param device_id: (str) Device ID of ONU to add
65 :param overwrite: (bool) Overwrite existing entry if found.
66
67 :raises KeyError: If device already exist and 'overwrite' is False
68 """
69 self.log.debug('add-device', device_id=device_id, overwrite=overwrite)
70
Zack Williams84a71e92019-11-15 09:00:19 -070071 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -060072 raise TypeError('Device ID should be an string')
73
74 if not self._started:
75 raise DatabaseStateError('The Database is not currently active')
76
77 if not overwrite and device_id in self._data:
78 raise KeyError('Device {} already exists in the database'
79 .format(device_id))
80
81 now = datetime.utcnow()
82 self._data[device_id] = {
83 DEVICE_ID_KEY: device_id,
84 CREATED_KEY: now,
85 LAST_SYNC_KEY: None,
86 MDS_KEY: 0,
87 VERSION_KEY: MibDbVolatileDict.CURRENT_VERSION,
88 ME_KEY: dict(),
89 MSG_TYPE_KEY: set()
90 }
91
92 def remove(self, device_id):
93 """
94 Remove an ONU from the database
95
96 :param device_id: (str) Device ID of ONU to remove from database
97 """
98 self.log.debug('remove-device', device_id=device_id)
99
Zack Williams84a71e92019-11-15 09:00:19 -0700100 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600101 raise TypeError('Device ID should be an string')
102
103 if not self._started:
104 raise DatabaseStateError('The Database is not currently active')
105
106 if device_id in self._data:
107 del self._data[device_id]
108 self._modified = datetime.utcnow()
109
110 def on_mib_reset(self, device_id):
111 """
112 Reset/clear the database for a specific Device
113
114 :param device_id: (str) ONU Device ID
115 :raises DatabaseStateError: If the database is not enabled
116 :raises KeyError: If the device does not exist in the database
117 """
118 if not self._started:
119 raise DatabaseStateError('The Database is not currently active')
120
Zack Williams84a71e92019-11-15 09:00:19 -0700121 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600122 raise TypeError('Device ID should be an string')
123
124 device_db = self._data[device_id]
125 self._modified = datetime.utcnow()
126
127 self._data[device_id] = {
128 DEVICE_ID_KEY: device_id,
129 CREATED_KEY: device_db[CREATED_KEY],
130 LAST_SYNC_KEY: device_db[LAST_SYNC_KEY],
131 MDS_KEY: 0,
132 VERSION_KEY: MibDbVolatileDict.CURRENT_VERSION,
133 ME_KEY: device_db[ME_KEY],
134 MSG_TYPE_KEY: device_db[MSG_TYPE_KEY]
135 }
136
137 def save_mib_data_sync(self, device_id, value):
138 """
139 Save the MIB Data Sync to the database in an easy location to access
140
141 :param device_id: (str) ONU Device ID
142 :param value: (int) Value to save
143 """
Zack Williams84a71e92019-11-15 09:00:19 -0700144 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600145 raise TypeError('Device ID should be an string')
146
147 if not isinstance(value, int):
148 raise TypeError('MIB Data Sync is an integer')
149
150 if not 0 <= value <= 255:
151 raise ValueError('Invalid MIB-data-sync value {}. Must be 0..255'.
152 format(value))
153
154 self._data[device_id][MDS_KEY] = value
155 self._modified = datetime.utcnow()
156
157 def get_mib_data_sync(self, device_id):
158 """
159 Get the MIB Data Sync value last saved to the database for a device
160
161 :param device_id: (str) ONU Device ID
162 :return: (int) The Value or None if not found
163 """
Zack Williams84a71e92019-11-15 09:00:19 -0700164 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600165 raise TypeError('Device ID should be an string')
166
167 if device_id not in self._data:
168 return None
169
170 return self._data[device_id].get(MDS_KEY)
171
172 def save_last_sync(self, device_id, value):
173 """
174 Save the Last Sync time to the database in an easy location to access
175
176 :param device_id: (str) ONU Device ID
177 :param value: (DateTime) Value to save
178 """
Zack Williams84a71e92019-11-15 09:00:19 -0700179 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600180 raise TypeError('Device ID should be an string')
181
182 if not isinstance(value, datetime):
183 raise TypeError('Expected a datetime object, got {}'.
184 format(type(datetime)))
185
186 self._data[device_id][LAST_SYNC_KEY] = value
187 self._modified = datetime.utcnow()
188
189 def get_last_sync(self, device_id):
190 """
191 Get the Last SYnc Time saved to the database for a device
192
193 :param device_id: (str) ONU Device ID
194 :return: (int) The Value or None if not found
195 """
Zack Williams84a71e92019-11-15 09:00:19 -0700196 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600197 raise TypeError('Device ID should be an string')
198
199 if device_id not in self._data:
200 return None
201
202 return self._data[device_id].get(LAST_SYNC_KEY)
203
204 def set(self, device_id, class_id, instance_id, attributes):
205 """
206 Set a database value. This should only be called by the MIB synchronizer
207 and its related tasks
208
209 :param device_id: (str) ONU Device ID
210 :param class_id: (int) ME Class ID
211 :param instance_id: (int) ME Entity ID
212 :param attributes: (dict) Attribute dictionary
213
214 :returns: (bool) True if the value was saved to the database. False if the
215 value was identical to the current instance
216
217 :raises KeyError: If device does not exist
218 :raises DatabaseStateError: If the database is not enabled
219 """
Zack Williams84a71e92019-11-15 09:00:19 -0700220 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600221 raise TypeError('Device ID should be a string')
222
223 if not 0 <= class_id <= 0xFFFF:
224 raise ValueError("Invalid Class ID: {}, should be 0..65535".format(class_id))
225
226 if not 0 <= instance_id <= 0xFFFF:
227 raise ValueError("Invalid Instance ID: {}, should be 0..65535".format(instance_id))
228
229 if not isinstance(attributes, dict):
230 raise TypeError("Attributes should be a dictionary")
231
232 if not self._started:
233 raise DatabaseStateError('The Database is not currently active')
234
235 now = datetime.utcnow()
236 try:
237 device_db = self._data[device_id]
238 class_db = device_db.get(class_id)
239 created = False
240
241 if class_db is None:
242 device_db[class_id] = {CLASS_ID_KEY: class_id}
243
244 class_db = device_db[class_id]
245 self._modified = now
246 created = True
247
248 instance_db = class_db.get(instance_id)
249 if instance_db is None:
250 class_db[instance_id] = {
251 INSTANCE_ID_KEY: instance_id,
252 CREATED_KEY: now,
253 MODIFIED_KEY: now,
254 ATTRIBUTES_KEY: dict()
255 }
256 instance_db = class_db[instance_id]
257 self._modified = now
258 created = True
259
260 changed = False
261
262 me_map = self._omci_agent.get_device(device_id).me_map
263 entity = me_map.get(class_id)
264
265 for attribute, value in attributes.items():
Zack Williams84a71e92019-11-15 09:00:19 -0700266 assert isinstance(attribute, six.string_types)
Chip Boling67b674a2019-02-08 11:42:18 -0600267 assert value is not None, "Attribute '{}' value cannot be 'None'".\
268 format(attribute)
269
270 db_value = instance_db[ATTRIBUTES_KEY].get(attribute) \
271 if ATTRIBUTES_KEY in instance_db else None
272
Zack Williams84a71e92019-11-15 09:00:19 -0700273 if entity is not None and isinstance(value, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600274 from scapy.fields import StrFixedLenField
275 attr_index = entity.attribute_name_to_index_map[attribute]
276 eca = entity.attributes[attr_index]
277 field = eca.field
278
279 if isinstance(field, StrFixedLenField):
280 from scapy.base_classes import Packet_metaclass
281 if isinstance(field.default, Packet_metaclass) \
282 and hasattr(field.default, 'json_from_value'):
283 # Value/hex of Packet Class to string
284 value = field.default.json_from_value(value)
285
286 if entity is not None and attribute in entity.attribute_name_to_index_map:
287 attr_index = entity.attribute_name_to_index_map[attribute]
288 eca = entity.attributes[attr_index]
289 field = eca.field
290
291 if hasattr(field, 'to_json'):
292 value = field.to_json(value, db_value)
293
294 # Complex packet types may have an attribute encoded as an object, this
295 # can be check by seeing if there is a to_json() conversion callable
296 # defined
297 if hasattr(value, 'to_json'):
298 value = value.to_json()
299
300 # Other complex packet types may be a repeated list field (FieldListField)
301 elif isinstance(value, (list, dict)):
302 value = json.dumps(value, separators=(',', ':'))
303
Matt Jeanneret40f28392019-12-04 18:21:46 -0500304 if isinstance(value, six.string_types):
305 value = value.rstrip('\x00')
306
307 if isinstance(value, six.binary_type):
308 value = value.decode('ascii').rstrip('\x00')
309
Chip Boling67b674a2019-02-08 11:42:18 -0600310 assert db_value is None or isinstance(value, type(db_value)), \
311 "New value type for attribute '{}' type is changing from '{}' to '{}'".\
312 format(attribute, type(db_value), type(value))
313
314 if db_value is None or db_value != value:
315 instance_db[ATTRIBUTES_KEY][attribute] = value
316 changed = True
317
318 if changed:
319 instance_db[MODIFIED_KEY] = now
320 self._modified = now
321
322 return changed or created
323
324 except Exception as e:
325 self.log.error('set-failure', e=e, class_id=class_id,
326 instance_id=instance_id, attributes=attributes)
327 raise
328
329 def delete(self, device_id, class_id, instance_id):
330 """
331 Delete an entity from the database if it exists. If all instances
332 of a class are deleted, the class is deleted as well.
333
334 :param device_id: (str) ONU Device ID
335 :param class_id: (int) ME Class ID
336 :param instance_id: (int) ME Entity ID
337
338 :returns: (bool) True if the instance was found and deleted. False
339 if it did not exist.
340
341 :raises KeyError: If device does not exist
342 :raises DatabaseStateError: If the database is not enabled
343 """
344 if not self._started:
345 raise DatabaseStateError('The Database is not currently active')
346
Zack Williams84a71e92019-11-15 09:00:19 -0700347 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600348 raise TypeError('Device ID should be an string')
349
350 if not 0 <= class_id <= 0xFFFF:
351 raise ValueError('class-id is 0..0xFFFF')
352
353 if not 0 <= instance_id <= 0xFFFF:
354 raise ValueError('instance-id is 0..0xFFFF')
355
356 try:
357 device_db = self._data[device_id]
358 class_db = device_db.get(class_id)
359
360 if class_db is None:
361 return False
362
363 instance_db = class_db.get(instance_id)
364 if instance_db is None:
365 return False
366
367 now = datetime.utcnow()
368 del class_db[instance_id]
369
370 if len(class_db) == 1: # Is only 'CLASS_ID_KEY' remaining
371 del device_db[class_id]
372
373 self._modified = now
374 return True
375
376 except Exception as e:
377 self.log.error('delete-failure', e=e)
378 raise
379
380 def query(self, device_id, class_id=None, instance_id=None, attributes=None):
381 """
382 Get database information.
383
384 This method can be used to request information from the database to the detailed
385 level requested
386
387 :param device_id: (str) ONU Device ID
388 :param class_id: (int) Managed Entity class ID
389 :param instance_id: (int) Managed Entity instance
390 :param attributes: (list/set or str) Managed Entity instance's attributes
391
392 :return: (dict) The value(s) requested. If class/inst/attribute is
393 not found, an empty dictionary is returned
394 :raises KeyError: If the requested device does not exist
395 :raises DatabaseStateError: If the database is not enabled
396 """
397 self.log.debug('query', device_id=device_id, class_id=class_id,
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400398 entity_instance_id=instance_id, attributes=attributes)
Chip Boling67b674a2019-02-08 11:42:18 -0600399
400 if not self._started:
401 raise DatabaseStateError('The Database is not currently active')
402
Zack Williams84a71e92019-11-15 09:00:19 -0700403 if not isinstance(device_id, six.string_types):
Chip Boling67b674a2019-02-08 11:42:18 -0600404 raise TypeError('Device ID is a string')
405
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400406 device_db = self._data.get(device_id, dict())
Chip Boling67b674a2019-02-08 11:42:18 -0600407 if class_id is None:
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400408 return self._fix_dev_json_attributes(copy.deepcopy(device_db), device_id)
Chip Boling67b674a2019-02-08 11:42:18 -0600409
410 if not isinstance(class_id, int):
411 raise TypeError('Class ID is an integer')
412
413 me_map = self._omci_agent.get_device(device_id).me_map
414 entity = me_map.get(class_id)
415
416 class_db = device_db.get(class_id, dict())
417 if instance_id is None or len(class_db) == 0:
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400418 return self._fix_cls_json_attributes(copy.deepcopy(class_db), entity)
Chip Boling67b674a2019-02-08 11:42:18 -0600419
420 if not isinstance(instance_id, int):
421 raise TypeError('Instance ID is an integer')
422
423 instance_db = class_db.get(instance_id, dict())
424 if attributes is None or len(instance_db) == 0:
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400425 return self._fix_inst_json_attributes(copy.deepcopy(instance_db), entity)
Chip Boling67b674a2019-02-08 11:42:18 -0600426
Zack Williams84a71e92019-11-15 09:00:19 -0700427 if not isinstance(attributes, (six.string_types, list, set)):
Chip Boling67b674a2019-02-08 11:42:18 -0600428 raise TypeError('Attributes should be a string or list/set of strings')
429
430 if not isinstance(attributes, (list, set)):
431 attributes = [attributes]
432
Zack Williams84a71e92019-11-15 09:00:19 -0700433 results = {attr: val for attr, val in six.iteritems(instance_db[ATTRIBUTES_KEY])
Chip Boling67b674a2019-02-08 11:42:18 -0600434 if attr in attributes}
435
436 for attr, attr_data in results.items():
437 attr_index = entity.attribute_name_to_index_map[attr]
438 eca = entity.attributes[attr_index]
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400439 results[attr] = self._fix_attr_json_attribute(copy.deepcopy(attr_data), eca)
Chip Boling67b674a2019-02-08 11:42:18 -0600440
441 return results
442
443 #########################################################################
444 # Following routines are used to fix-up JSON encoded complex data. A
445 # nice side effect is that the values returned will be a deep-copy of
446 # the class/instance/attribute data of what is in the database. Note
447 # That other database values (created, modified, ...) will still reference
448 # back to the original DB.
449
450 def _fix_dev_json_attributes(self, dev_data, device_id):
451 for cls_id, cls_data in dev_data.items():
452 if isinstance(cls_id, int):
453 me_map = self._omci_agent.get_device(device_id).me_map
454 entity = me_map.get(cls_id)
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400455 dev_data[cls_id] = self._fix_cls_json_attributes(copy.deepcopy(cls_data), entity)
Chip Boling67b674a2019-02-08 11:42:18 -0600456 return dev_data
457
458 def _fix_cls_json_attributes(self, cls_data, entity):
459 for inst_id, inst_data in cls_data.items():
460 if isinstance(inst_id, int):
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400461 cls_data[inst_id] = self._fix_inst_json_attributes(copy.deepcopy(inst_data), entity)
Chip Boling67b674a2019-02-08 11:42:18 -0600462 return cls_data
463
464 def _fix_inst_json_attributes(self, inst_data, entity):
465 if ATTRIBUTES_KEY in inst_data:
466 for attr, attr_data in inst_data[ATTRIBUTES_KEY].items():
467 attr_index = entity.attribute_name_to_index_map[attr] \
468 if entity is not None and attr in entity.attribute_name_to_index_map else None
469 eca = entity.attributes[attr_index] if attr_index is not None else None
Matt Jeanneret2bcbab02019-09-23 07:28:49 -0400470 inst_data[ATTRIBUTES_KEY][attr] = self._fix_attr_json_attribute(copy.deepcopy(attr_data), eca)
Chip Boling67b674a2019-02-08 11:42:18 -0600471 return inst_data
472
473 def _fix_attr_json_attribute(self, attr_data, eca):
474
475 try:
Matt Jeanneret40f28392019-12-04 18:21:46 -0500476 if eca is not None and hasattr(eca.field, 'load_json'):
477 try:
478 value = eca.field.load_json(attr_data)
Chip Boling67b674a2019-02-08 11:42:18 -0600479 return value
Matt Jeanneret40f28392019-12-04 18:21:46 -0500480 except ValueError:
481 pass
Chip Boling67b674a2019-02-08 11:42:18 -0600482
Matt Jeanneret40f28392019-12-04 18:21:46 -0500483 if isinstance(attr_data, six.string_types):
484 try:
485 value = json.loads(attr_data)
486 return value
487 except ValueError:
488 pass
Chip Boling67b674a2019-02-08 11:42:18 -0600489
Chip Boling67b674a2019-02-08 11:42:18 -0600490 return attr_data
491
492 except Exception as e:
Matt Jeanneret40f28392019-12-04 18:21:46 -0500493 self.log.error('could-not-parse-attribute-returning-as-is', field=eca.field, attr_data=attr_data, e=e)
494 return attr_data
Chip Boling67b674a2019-02-08 11:42:18 -0600495
496 def update_supported_managed_entities(self, device_id, managed_entities):
497 """
498 Update the supported OMCI Managed Entities for this device
499
500 :param device_id: (str) ONU Device ID
501 :param managed_entities: (set) Managed Entity class IDs
502 """
503 now = datetime.utcnow()
504 try:
505 device_db = self._data[device_id]
506
507 entities = {class_id: self._managed_entity_to_name(device_id, class_id)
508 for class_id in managed_entities}
509
510 device_db[ME_KEY] = entities
511 self._modified = now
512
513 except Exception as e:
514 self.log.error('set-me-failure', e=e)
515 raise
516
517 def _managed_entity_to_name(self, device_id, class_id):
518 me_map = self._omci_agent.get_device(device_id).me_map
519 entity = me_map.get(class_id)
520
521 return entity.__name__ if entity is not None else 'UnknownManagedEntity'
522
523 def update_supported_message_types(self, device_id, msg_types):
524 """
525 Update the supported OMCI Managed Entities for this device
526
527 :param device_id: (str) ONU Device ID
528 :param msg_types: (set) Message Type values (ints)
529 """
530 now = datetime.utcnow()
531 try:
532 msg_type_set = {msg_type.value for msg_type in msg_types}
533 self._data[device_id][MSG_TYPE_KEY] = msg_type_set
534 self._modified = now
535
536 except Exception as e:
537 self.log.error('set-me-failure', e=e)
538 raise
Matt Jeanneret40f28392019-12-04 18:21:46 -0500539
540 def load_from_template(self, device_id, template):
541 now = datetime.utcnow()
542 headerdata = {
543 DEVICE_ID_KEY: device_id,
544 CREATED_KEY: now,
545 LAST_SYNC_KEY: None,
546 MDS_KEY: 0,
547 VERSION_KEY: MibDbVolatileDict.CURRENT_VERSION,
548 ME_KEY: dict(),
549 MSG_TYPE_KEY: set()
550 }
551 template.update(headerdata)
552 self._data[device_id] = template
553
554 def dump_to_json(self, device_id):
555 device_db = self._data.get(device_id, dict())
Matt Jeanneret40f28392019-12-04 18:21:46 -0500556
557 def json_converter(o):
558 if isinstance(o, datetime):
559 return o.__str__()
560 if isinstance(o, six.binary_type):
561 return o.decode('ascii')
562
Matt Jeanneretc233b2e2019-12-07 15:46:11 -0500563 json_string = json.dumps(device_db, default=json_converter, indent=2)
Matt Jeanneret40f28392019-12-04 18:21:46 -0500564
565 return json_string