Source code for cinderlib.serialization

# Copyright (c) 2018, Red Hat, Inc.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Oslo Versioned Objects helper file.

These methods help with the serialization of Cinderlib objects that uses the
OVO serialization mechanism, so we remove circular references when doing the
JSON serialization of objects (for example in a Volume OVO it has a 'snapshot'
field which is a Snapshot OVO that has a 'volume' back reference), piggy back
on the OVO's serialization mechanism to add/get additional data we want.
"""

import functools
import json as json_lib
import six

from cinder.objects import base as cinder_base_ovo
from oslo_versionedobjects import base as base_ovo
from oslo_versionedobjects import fields as ovo_fields

from cinderlib import objects


# Variable used to avoid circular references
BACKEND_CLASS = None


[docs]def setup(backend_class): global BACKEND_CLASS BACKEND_CLASS = backend_class # Use custom dehydration methods that prevent maximum recursion errors # due to circular references: # ie: snapshot -> volume -> snapshots -> snapshot base_ovo.VersionedObject.obj_to_primitive = obj_to_primitive cinder_base_ovo.CinderObject.obj_from_primitive = classmethod( obj_from_primitive) fields = base_ovo.obj_fields fields.Object.to_primitive = staticmethod(field_ovo_to_primitive) fields.Field.to_primitive = field_to_primitive fields.List.to_primitive = iterable_to_primitive fields.Set.to_primitive = iterable_to_primitive fields.Dict.to_primitive = dict_to_primitive fields.DateTime.to_primitive = staticmethod(datetime_to_primitive) wrap_to_primitive(fields.FieldType) wrap_to_primitive(fields.IPAddress)
[docs]def wrap_to_primitive(cls): method = getattr(cls, 'to_primitive') @functools.wraps(method) def to_primitive(obj, attr, value, visited=None): return method(obj, attr, value) setattr(cls, 'to_primitive', staticmethod(to_primitive))
def _set_visited(element, visited): # visited keeps track of elements visited to prevent loops if visited is None: visited = set() # We only care about complex object that can have loops, others are ignored # to prevent us from not serializing simple objects, such as booleans, that # can have the same instance used for multiple fields. if isinstance(element, (ovo_fields.ObjectField, cinder_base_ovo.CinderObject)): visited.add(id(element)) return visited
[docs]def obj_to_primitive(self, target_version=None, version_manifest=None, visited=None): # No target_version, version_manifest, or changes support visited = _set_visited(self, visited) primitive = {} for name, field in self.fields.items(): if self.obj_attr_is_set(name): value = getattr(self, name) # Skip cycles if id(value) in visited: continue primitive[name] = field.to_primitive(self, name, value, visited) obj_name = self.obj_name() obj = { self._obj_primitive_key('name'): obj_name, self._obj_primitive_key('namespace'): self.OBJ_PROJECT_NAMESPACE, self._obj_primitive_key('version'): self.VERSION, self._obj_primitive_key('data'): primitive } # Piggyback to store our own data cl_obj = getattr(self, '_cl_obj', None) clib_data = cl_obj and cl_obj._to_primitive() if clib_data: obj['cinderlib.data'] = clib_data return obj
[docs]def obj_from_primitive( cls, primitive, context=None, original_method=cinder_base_ovo.CinderObject.obj_from_primitive): result = original_method(primitive, context) result.cinderlib_data = primitive.get('cinderlib.data') return result
[docs]def field_ovo_to_primitive(obj, attr, value, visited=None): return value.obj_to_primitive(visited=visited)
[docs]def field_to_primitive(self, obj, attr, value, visited=None): if value is None: return None return self._type.to_primitive(obj, attr, value, visited)
[docs]def iterable_to_primitive(self, obj, attr, value, visited=None): visited = _set_visited(self, visited) result = [] for elem in value: if id(elem) in visited: continue _set_visited(elem, visited) r = self._element_type.to_primitive(obj, attr, elem, visited) result.append(r) return result
[docs]def dict_to_primitive(self, obj, attr, value, visited=None): visited = _set_visited(self, visited) primitive = {} for key, elem in value.items(): if id(elem) in visited: continue _set_visited(elem, visited) primitive[key] = self._element_type.to_primitive( obj, '%s["%s"]' % (attr, key), elem, visited) return primitive
[docs]def datetime_to_primitive(obj, attr, value, visited=None): """Stringify time in ISO 8601 with subsecond format. This is the same code as the one used by the OVO DateTime to_primitive but adding the subsecond resolution with the '.%f' part in strftime call. This is backward compatible with cinderlib using code that didn't generate subsecond resolution, because the from_primitive code of the OVO field uses oslo_utils.timeutils.parse_isotime which in the end uses iso8601.parse_date, and since the subsecond format is also ISO8601 it is properly parsed. """ st = value.strftime('%Y-%m-%dT%H:%M:%S.%f') tz = value.tzinfo.tzname(None) if value.tzinfo else 'UTC' # Need to handle either iso8601 or python UTC format st += ('Z' if tz in ['UTC', 'UTC+00:00'] else tz) return st
[docs]def load(json_src, save=False): """Load any json serialized cinderlib object.""" if isinstance(json_src, six.string_types): json_src = json_lib.loads(json_src) if isinstance(json_src, list): return [getattr(objects, obj['class']).load(obj, save) for obj in json_src] return getattr(objects, json_src['class']).load(json_src, save)
[docs]def json(): """Convert to Json everything we have in this system.""" return [backend.json for backend in BACKEND_CLASS.backends.values()]
[docs]def jsons(): """Convert to a Json string everything we have in this system.""" return json_lib.dumps(json(), separators=(',', ':'))
[docs]def dump(): """Convert to Json everything we have in this system.""" return [backend.dump for backend in BACKEND_CLASS.backends.values()]
[docs]def dumps(): """Convert to a Json string everything we have in this system.""" return json_lib.dumps(dump(), separators=(',', ':'))