Switch to oslo_versionedobjects
oslo_versionedobjects was not made available until very late in the Kilo cycle (i.e. near the end of kilo-3). In order to make progress on cinder objects, a fork of nova objects was made, so that proper trial and testing could be done. The following patch makes the switch to use oslo_versionedobjects. Implements: blueprint cinder-objects Change-Id: I883f387c8247e8d79da82016a624cef2180cde88
This commit is contained in:
parent
88f4c0f058
commit
4f63f7ff48
@ -26,6 +26,7 @@ import sys
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_versionedobjects import exception as obj_exc
|
||||||
import six
|
import six
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
@ -663,28 +664,12 @@ class EvaluatorParseException(Exception):
|
|||||||
message = _("Error during evaluator parsing: %(reason)s")
|
message = _("Error during evaluator parsing: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
class ObjectActionError(CinderException):
|
UnsupportedObjectError = obj_exc.UnsupportedObjectError
|
||||||
msg_fmt = _('Object action %(action)s failed because: %(reason)s')
|
OrphanedObjectError = obj_exc.OrphanedObjectError
|
||||||
|
IncompatibleObjectVersion = obj_exc.IncompatibleObjectVersion
|
||||||
|
ReadOnlyFieldError = obj_exc.ReadOnlyFieldError
|
||||||
class ObjectFieldInvalid(CinderException):
|
ObjectActionError = obj_exc.ObjectActionError
|
||||||
msg_fmt = _('Field %(field)s of %(objname)s is not an instance of Field')
|
ObjectFieldInvalid = obj_exc.ObjectFieldInvalid
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedObjectError(CinderException):
|
|
||||||
msg_fmt = _('Unsupported object type %(objtype)s')
|
|
||||||
|
|
||||||
|
|
||||||
class OrphanedObjectError(CinderException):
|
|
||||||
msg_fmt = _('Cannot call %(method)s on orphaned %(objtype)s object')
|
|
||||||
|
|
||||||
|
|
||||||
class IncompatibleObjectVersion(CinderException):
|
|
||||||
msg_fmt = _('Version %(objver)s of %(objname)s is not supported')
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyFieldError(CinderException):
|
|
||||||
msg_fmt = _('Cannot modify readonly field %(field)s')
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeGroupNotFound(CinderException):
|
class VolumeGroupNotFound(CinderException):
|
||||||
|
@ -14,550 +14,42 @@
|
|||||||
|
|
||||||
"""Cinder common internal object model"""
|
"""Cinder common internal object model"""
|
||||||
|
|
||||||
import collections
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import netaddr
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import oslo_messaging as messaging
|
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
|
from oslo_versionedobjects import base
|
||||||
|
from oslo_versionedobjects import fields
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _, _LE
|
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.objects import fields
|
|
||||||
from cinder.openstack.common import versionutils
|
|
||||||
from cinder import utils
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger('object')
|
LOG = logging.getLogger('object')
|
||||||
|
remotable = base.remotable
|
||||||
|
remotable_classmethod = base.remotable_classmethod
|
||||||
|
obj_make_list = base.obj_make_list
|
||||||
|
|
||||||
|
|
||||||
class NotSpecifiedSentinel(object):
|
class CinderObjectRegistry(base.VersionedObjectRegistry):
|
||||||
pass
|
def registration_hook(self, cls, index):
|
||||||
|
setattr(objects, cls.obj_name(), cls)
|
||||||
|
|
||||||
|
|
||||||
def get_attrname(name):
|
@CinderObjectRegistry.register
|
||||||
"""Return the mangled name of the attribute's underlying storage."""
|
class CinderObject(base.VersionedObject):
|
||||||
return '_' + name
|
# NOTE(thangp): OBJ_PROJECT_NAMESPACE needs to be set so that nova,
|
||||||
|
# cinder, and other objects can exist on the same bus and be distinguished
|
||||||
|
# from one another.
|
||||||
|
OBJ_PROJECT_NAMESPACE = 'cinder'
|
||||||
|
|
||||||
|
|
||||||
def make_class_properties(cls):
|
class CinderObjectDictCompat(base.VersionedObjectDictCompat):
|
||||||
# NOTE(danms/comstud): Inherit fields from super classes.
|
"""Mix-in to provide dictionary key access compat.
|
||||||
# mro() returns the current class first and returns 'object' last, so
|
|
||||||
# those can be skipped. Also be careful to not overwrite any fields
|
|
||||||
# that already exist. And make sure each cls has its own copy of
|
|
||||||
# fields and that it is not sharing the dict with a super class.
|
|
||||||
cls.fields = dict(cls.fields)
|
|
||||||
for supercls in cls.mro()[1:-1]:
|
|
||||||
if not hasattr(supercls, 'fields'):
|
|
||||||
continue
|
|
||||||
for name, field in supercls.fields.items():
|
|
||||||
if name not in cls.fields:
|
|
||||||
cls.fields[name] = field
|
|
||||||
for name, field in cls.fields.iteritems():
|
|
||||||
if not isinstance(field, fields.Field):
|
|
||||||
raise exception.ObjectFieldInvalid(
|
|
||||||
field=name, objname=cls.obj_name())
|
|
||||||
|
|
||||||
def getter(self, name=name):
|
|
||||||
attrname = get_attrname(name)
|
|
||||||
if not hasattr(self, attrname):
|
|
||||||
self.obj_load_attr(name)
|
|
||||||
return getattr(self, attrname)
|
|
||||||
|
|
||||||
def setter(self, value, name=name, field=field):
|
|
||||||
attrname = get_attrname(name)
|
|
||||||
field_value = field.coerce(self, name, value)
|
|
||||||
if field.read_only and hasattr(self, attrname):
|
|
||||||
# Note(yjiang5): _from_db_object() may iterate
|
|
||||||
# every field and write, no exception in such situation.
|
|
||||||
if getattr(self, attrname) != field_value:
|
|
||||||
raise exception.ReadOnlyFieldError(field=name)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._changed_fields.add(name)
|
|
||||||
try:
|
|
||||||
return setattr(self, attrname, field_value)
|
|
||||||
except Exception:
|
|
||||||
attr = "%s.%s" % (self.obj_name(), name)
|
|
||||||
LOG.exception(_LE('Error setting %(attr)s'), {'attr': attr})
|
|
||||||
raise
|
|
||||||
|
|
||||||
setattr(cls, name, property(getter, setter))
|
|
||||||
|
|
||||||
|
|
||||||
class CinderObjectMetaclass(type):
|
|
||||||
"""Metaclass that allows tracking of object classes."""
|
|
||||||
|
|
||||||
# NOTE(danms): This is what controls whether object operations are
|
|
||||||
# remoted. If this is not None, use it to remote things over RPC.
|
|
||||||
indirection_api = None
|
|
||||||
|
|
||||||
def __init__(cls, names, bases, dict_):
|
|
||||||
if not hasattr(cls, '_obj_classes'):
|
|
||||||
# This means this is a base class using the metaclass. I.e.,
|
|
||||||
# the 'CinderObject' class.
|
|
||||||
cls._obj_classes = collections.defaultdict(list)
|
|
||||||
return
|
|
||||||
|
|
||||||
def _vers_tuple(obj):
|
|
||||||
return tuple([int(x) for x in obj.VERSION.split(".")])
|
|
||||||
|
|
||||||
# Add the subclass to CinderObject._obj_classes. If the
|
|
||||||
# same version already exists, replace it. Otherwise,
|
|
||||||
# keep the list with newest version first.
|
|
||||||
make_class_properties(cls)
|
|
||||||
obj_name = cls.obj_name()
|
|
||||||
for i, obj in enumerate(cls._obj_classes[obj_name]):
|
|
||||||
if cls.VERSION == obj.VERSION:
|
|
||||||
cls._obj_classes[obj_name][i] = cls
|
|
||||||
# Update cinder.objects with this newer class.
|
|
||||||
setattr(objects, obj_name, cls)
|
|
||||||
break
|
|
||||||
if _vers_tuple(cls) > _vers_tuple(obj):
|
|
||||||
# Insert before.
|
|
||||||
cls._obj_classes[obj_name].insert(i, cls)
|
|
||||||
if i == 0:
|
|
||||||
# Later version than we've seen before. Update
|
|
||||||
# cinder.objects.
|
|
||||||
setattr(objects, obj_name, cls)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
cls._obj_classes[obj_name].append(cls)
|
|
||||||
# Either this is the first time we've seen the object or it's
|
|
||||||
# an older version than anything we'e seen. Update cinder.objects
|
|
||||||
# only if it's the first time we've seen this object name.
|
|
||||||
if not hasattr(objects, obj_name):
|
|
||||||
setattr(objects, obj_name, cls)
|
|
||||||
|
|
||||||
|
|
||||||
# These are decorators that mark an object's method as remotable.
|
|
||||||
# If the metaclass is configured to forward object methods to an
|
|
||||||
# indirection service, these will result in making an RPC call
|
|
||||||
# instead of directly calling the implementation in the object. Instead,
|
|
||||||
# the object implementation on the remote end will perform the
|
|
||||||
# requested action and the result will be returned here.
|
|
||||||
def remotable_classmethod(fn):
|
|
||||||
"""Decorator for remotable classmethods."""
|
|
||||||
@functools.wraps(fn)
|
|
||||||
def wrapper(cls, context, *args, **kwargs):
|
|
||||||
if CinderObject.indirection_api:
|
|
||||||
result = CinderObject.indirection_api.object_class_action(
|
|
||||||
context, cls.obj_name(), fn.__name__, cls.VERSION,
|
|
||||||
args, kwargs)
|
|
||||||
else:
|
|
||||||
result = fn(cls, context, *args, **kwargs)
|
|
||||||
if isinstance(result, CinderObject):
|
|
||||||
result._context = context
|
|
||||||
return result
|
|
||||||
|
|
||||||
# NOTE(danms): Make this discoverable
|
|
||||||
wrapper.remotable = True
|
|
||||||
wrapper.original_fn = fn
|
|
||||||
return classmethod(wrapper)
|
|
||||||
|
|
||||||
|
|
||||||
# See comment above for remotable_classmethod()
|
|
||||||
#
|
|
||||||
# Note that this will use either the provided context, or the one
|
|
||||||
# stashed in the object. If neither are present, the object is
|
|
||||||
# "orphaned" and remotable methods cannot be called.
|
|
||||||
def remotable(fn):
|
|
||||||
"""Decorator for remotable object methods."""
|
|
||||||
@functools.wraps(fn)
|
|
||||||
def wrapper(self, *args, **kwargs):
|
|
||||||
ctxt = self._context
|
|
||||||
try:
|
|
||||||
if isinstance(args[0], (context.RequestContext)):
|
|
||||||
ctxt = args[0]
|
|
||||||
args = args[1:]
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
if ctxt is None:
|
|
||||||
raise exception.OrphanedObjectError(method=fn.__name__,
|
|
||||||
objtype=self.obj_name())
|
|
||||||
# Force this to be set if it wasn't before.
|
|
||||||
self._context = ctxt
|
|
||||||
if CinderObject.indirection_api:
|
|
||||||
updates, result = CinderObject.indirection_api.object_action(
|
|
||||||
ctxt, self, fn.__name__, args, kwargs)
|
|
||||||
for key, value in updates.iteritems():
|
|
||||||
if key in self.fields:
|
|
||||||
field = self.fields[key]
|
|
||||||
# NOTE(ndipanov): Since CinderObjectSerializer will have
|
|
||||||
# deserialized any object fields into objects already,
|
|
||||||
# we do not try to deserialize them again here.
|
|
||||||
if isinstance(value, CinderObject):
|
|
||||||
self[key] = value
|
|
||||||
else:
|
|
||||||
self[key] = field.from_primitive(self, key, value)
|
|
||||||
self.obj_reset_changes()
|
|
||||||
self._changed_fields = set(updates.get('obj_what_changed', []))
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
return fn(self, ctxt, *args, **kwargs)
|
|
||||||
|
|
||||||
wrapper.remotable = True
|
|
||||||
wrapper.original_fn = fn
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(CinderObjectMetaclass)
|
|
||||||
class CinderObject(object):
|
|
||||||
"""Base class and object factory.
|
|
||||||
|
|
||||||
This forms the base of all objects that can be remoted or instantiated
|
|
||||||
via RPC. Simply defining a class that inherits from this base class
|
|
||||||
will make it remotely instantiatable. Objects should implement the
|
|
||||||
necessary "get" classmethod routines as well as "save" object methods
|
|
||||||
as appropriate.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Object versioning rules
|
|
||||||
#
|
|
||||||
# Each service has its set of objects, each with a version attached. When
|
|
||||||
# a client attempts to call an object method, the server checks to see if
|
|
||||||
# the version of that object matches (in a compatible way) its object
|
|
||||||
# implementation. If so, cool, and if not, fail.
|
|
||||||
#
|
|
||||||
# This version is allowed to have three parts, X.Y.Z, where the .Z element
|
|
||||||
# is reserved for stable branch backports. The .Z is ignored for the
|
|
||||||
# purposes of triggering a backport, which means anything changed under
|
|
||||||
# a .Z must be additive and non-destructive such that a node that knows
|
|
||||||
# about X.Y can consider X.Y.Z equivalent.
|
|
||||||
VERSION = '1.0'
|
|
||||||
|
|
||||||
# The fields present in this object as key:field pairs. For example:
|
|
||||||
#
|
|
||||||
# fields = { 'foo': fields.IntegerField(),
|
|
||||||
# 'bar': fields.StringField(),
|
|
||||||
# }
|
|
||||||
fields = {}
|
|
||||||
obj_extra_fields = []
|
|
||||||
|
|
||||||
# Table of sub-object versioning information
|
|
||||||
#
|
|
||||||
# This contains a list of version mappings, by the field name of
|
|
||||||
# the subobject. The mappings must be in order of oldest to
|
|
||||||
# newest, and are tuples of (my_version, subobject_version). A
|
|
||||||
# request to backport this object to $my_version will cause the
|
|
||||||
# subobject to be backported to $subobject_version.
|
|
||||||
#
|
|
||||||
# obj_relationships = {
|
|
||||||
# 'subobject1': [('1.2', '1.1'), ('1.4', '1.2')],
|
|
||||||
# 'subobject2': [('1.2', '1.0')],
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# In the above example:
|
|
||||||
#
|
|
||||||
# - If we are asked to backport our object to version 1.3,
|
|
||||||
# subobject1 will be backported to version 1.1, since it was
|
|
||||||
# bumped to version 1.2 when our version was 1.4.
|
|
||||||
# - If we are asked to backport our object to version 1.5,
|
|
||||||
# no changes will be made to subobject1 or subobject2, since
|
|
||||||
# they have not changed since version 1.4.
|
|
||||||
# - If we are asked to backlevel our object to version 1.1, we
|
|
||||||
# will remove both subobject1 and subobject2 from the primitive,
|
|
||||||
# since they were not added until version 1.2.
|
|
||||||
obj_relationships = {}
|
|
||||||
|
|
||||||
def __init__(self, context=None, **kwargs):
|
|
||||||
self._changed_fields = set()
|
|
||||||
self._context = context
|
|
||||||
for key in kwargs.keys():
|
|
||||||
setattr(self, key, kwargs[key])
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s(%s)' % (
|
|
||||||
self.obj_name(),
|
|
||||||
','.join(['%s=%s' % (name,
|
|
||||||
(self.obj_attr_is_set(name) and
|
|
||||||
field.stringify(getattr(self, name)) or
|
|
||||||
'<?>'))
|
|
||||||
for name, field in sorted(self.fields.items())]))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def obj_name(cls):
|
|
||||||
"""Return a canonical name for this object.
|
|
||||||
|
|
||||||
The canonical name will be used over the wire for remote hydration.
|
|
||||||
"""
|
|
||||||
return cls.__name__
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def obj_class_from_name(cls, objname, objver):
|
|
||||||
"""Returns a class from the registry based on a name and version."""
|
|
||||||
if objname not in cls._obj_classes:
|
|
||||||
LOG.error(_LE('Unable to instantiate unregistered object type '
|
|
||||||
'%(objtype)s'), dict(objtype=objname))
|
|
||||||
raise exception.UnsupportedObjectError(objtype=objname)
|
|
||||||
|
|
||||||
# NOTE(comstud): If there's not an exact match, return the highest
|
|
||||||
# compatible version. The objects stored in the class are sorted
|
|
||||||
# such that highest version is first, so only set compatible_match
|
|
||||||
# once below.
|
|
||||||
compatible_match = None
|
|
||||||
|
|
||||||
for objclass in cls._obj_classes[objname]:
|
|
||||||
if objclass.VERSION == objver:
|
|
||||||
return objclass
|
|
||||||
if (not compatible_match and
|
|
||||||
versionutils.is_compatible(objver, objclass.VERSION)):
|
|
||||||
compatible_match = objclass
|
|
||||||
|
|
||||||
if compatible_match:
|
|
||||||
return compatible_match
|
|
||||||
|
|
||||||
# As mentioned above, latest version is always first in the list.
|
|
||||||
latest_ver = cls._obj_classes[objname][0].VERSION
|
|
||||||
raise exception.IncompatibleObjectVersion(objname=objname,
|
|
||||||
objver=objver,
|
|
||||||
supported=latest_ver)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _obj_from_primitive(cls, context, objver, primitive):
|
|
||||||
self = cls()
|
|
||||||
self._context = context
|
|
||||||
self.VERSION = objver
|
|
||||||
objdata = primitive['cinder_object.data']
|
|
||||||
changes = primitive.get('cinder_object.changes', [])
|
|
||||||
for name, field in self.fields.items():
|
|
||||||
if name in objdata:
|
|
||||||
setattr(self, name, field.from_primitive(self, name,
|
|
||||||
objdata[name]))
|
|
||||||
self._changed_fields = set([x for x in changes if x in self.fields])
|
|
||||||
return self
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def obj_from_primitive(cls, primitive, context=None):
|
|
||||||
"""Object field-by-field hydration."""
|
|
||||||
if primitive['cinder_object.namespace'] != 'cinder':
|
|
||||||
# NOTE(danms): We don't do anything with this now, but it's
|
|
||||||
# there for "the future"
|
|
||||||
raise exception.UnsupportedObjectError(
|
|
||||||
objtype='%s.%s' % (primitive['cinder_object.namespace'],
|
|
||||||
primitive['cinder_object.name']))
|
|
||||||
objname = primitive['cinder_object.name']
|
|
||||||
objver = primitive['cinder_object.version']
|
|
||||||
objclass = cls.obj_class_from_name(objname, objver)
|
|
||||||
return objclass._obj_from_primitive(context, objver, primitive)
|
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
|
||||||
"""Efficiently make a deep copy of this object."""
|
|
||||||
|
|
||||||
# NOTE(danms): A naive deepcopy would copy more than we need,
|
|
||||||
# and since we have knowledge of the volatile bits of the
|
|
||||||
# object, we can be smarter here. Also, nested entities within
|
|
||||||
# some objects may be uncopyable, so we can avoid those sorts
|
|
||||||
# of issues by copying only our field data.
|
|
||||||
|
|
||||||
nobj = self.__class__()
|
|
||||||
nobj._context = self._context
|
|
||||||
for name in self.fields:
|
|
||||||
if self.obj_attr_is_set(name):
|
|
||||||
nval = copy.deepcopy(getattr(self, name), memo)
|
|
||||||
setattr(nobj, name, nval)
|
|
||||||
nobj._changed_fields = set(self._changed_fields)
|
|
||||||
return nobj
|
|
||||||
|
|
||||||
def obj_clone(self):
|
|
||||||
"""Create a copy."""
|
|
||||||
return copy.deepcopy(self)
|
|
||||||
|
|
||||||
def _obj_make_obj_compatible(self, primitive, target_version, field):
|
|
||||||
"""Backlevel a sub-object based on our versioning rules.
|
|
||||||
|
|
||||||
This is responsible for backporting objects contained within
|
|
||||||
this object's primitive according to a set of rules we
|
|
||||||
maintain about version dependencies between objects. This
|
|
||||||
requires that the obj_relationships table in this object is
|
|
||||||
correct and up-to-date.
|
|
||||||
|
|
||||||
:param:primitive: The primitive version of this object
|
|
||||||
:param:target_version: The version string requested for this object
|
|
||||||
:param:field: The name of the field in this object containing the
|
|
||||||
sub-object to be backported
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _do_backport(to_version):
|
|
||||||
obj = getattr(self, field)
|
|
||||||
if not obj:
|
|
||||||
return
|
|
||||||
if isinstance(obj, CinderObject):
|
|
||||||
obj.obj_make_compatible(
|
|
||||||
primitive[field]['cinder_object.data'],
|
|
||||||
to_version)
|
|
||||||
primitive[field]['cinder_object.version'] = to_version
|
|
||||||
elif isinstance(obj, list):
|
|
||||||
for i, element in enumerate(obj):
|
|
||||||
element.obj_make_compatible(
|
|
||||||
primitive[field][i]['cinder_object.data'],
|
|
||||||
to_version)
|
|
||||||
primitive[field][i]['cinder_object.version'] = to_version
|
|
||||||
|
|
||||||
target_version = utils.convert_version_to_tuple(target_version)
|
|
||||||
for index, versions in enumerate(self.obj_relationships[field]):
|
|
||||||
my_version, child_version = versions
|
|
||||||
my_version = utils.convert_version_to_tuple(my_version)
|
|
||||||
if target_version < my_version:
|
|
||||||
if index == 0:
|
|
||||||
# We're backporting to a version from before this
|
|
||||||
# subobject was added: delete it from the primitive.
|
|
||||||
del primitive[field]
|
|
||||||
else:
|
|
||||||
# We're in the gap between index-1 and index, so
|
|
||||||
# backport to the older version
|
|
||||||
last_child_version = \
|
|
||||||
self.obj_relationships[field][index - 1][1]
|
|
||||||
_do_backport(last_child_version)
|
|
||||||
return
|
|
||||||
elif target_version == my_version:
|
|
||||||
# This is the first mapping that satisfies the
|
|
||||||
# target_version request: backport the object.
|
|
||||||
_do_backport(child_version)
|
|
||||||
return
|
|
||||||
|
|
||||||
def obj_make_compatible(self, primitive, target_version):
|
|
||||||
"""Make an object representation compatible with a target version.
|
|
||||||
|
|
||||||
This is responsible for taking the primitive representation of
|
|
||||||
an object and making it suitable for the given target_version.
|
|
||||||
This may mean converting the format of object attributes, removing
|
|
||||||
attributes that have been added since the target version, etc. In
|
|
||||||
general:
|
|
||||||
|
|
||||||
- If a new version of an object adds a field, this routine
|
|
||||||
should remove it for older versions.
|
|
||||||
- If a new version changed or restricted the format of a field, this
|
|
||||||
should convert it back to something a client knowing only of the
|
|
||||||
older version will tolerate.
|
|
||||||
- If an object that this object depends on is bumped, then this
|
|
||||||
object should also take a version bump. Then, this routine should
|
|
||||||
backlevel the dependent object (by calling its obj_make_compatible())
|
|
||||||
if the requested version of this object is older than the version
|
|
||||||
where the new dependent object was added.
|
|
||||||
|
|
||||||
:param:primitive: The result of self.obj_to_primitive()
|
|
||||||
:param:target_version: The version string requested by the recipient
|
|
||||||
of the object
|
|
||||||
:raises: cinder.exception.UnsupportedObjectError if conversion
|
|
||||||
is not possible for some reason
|
|
||||||
"""
|
|
||||||
for key, field in self.fields.items():
|
|
||||||
if not isinstance(field, (fields.ObjectField,
|
|
||||||
fields.ListOfObjectsField)):
|
|
||||||
continue
|
|
||||||
if not self.obj_attr_is_set(key):
|
|
||||||
continue
|
|
||||||
if key not in self.obj_relationships:
|
|
||||||
# NOTE(danms): This is really a coding error and shouldn't
|
|
||||||
# happen unless we miss something
|
|
||||||
raise exception.ObjectActionError(
|
|
||||||
action='obj_make_compatible',
|
|
||||||
reason='No rule for %s' % key)
|
|
||||||
self._obj_make_obj_compatible(primitive, target_version, key)
|
|
||||||
|
|
||||||
def obj_to_primitive(self, target_version=None):
|
|
||||||
"""Simple base-case dehydration.
|
|
||||||
|
|
||||||
This calls to_primitive() for each item in fields.
|
|
||||||
"""
|
|
||||||
primitive = dict()
|
|
||||||
for name, field in self.fields.items():
|
|
||||||
if self.obj_attr_is_set(name):
|
|
||||||
primitive[name] = field.to_primitive(self, name,
|
|
||||||
getattr(self, name))
|
|
||||||
if target_version:
|
|
||||||
self.obj_make_compatible(primitive, target_version)
|
|
||||||
obj = {'cinder_object.name': self.obj_name(),
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': target_version or self.VERSION,
|
|
||||||
'cinder_object.data': primitive}
|
|
||||||
if self.obj_what_changed():
|
|
||||||
obj['cinder_object.changes'] = sorted(self.obj_what_changed())
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def obj_set_defaults(self, *attrs):
|
|
||||||
if not attrs:
|
|
||||||
attrs = [name for name, field in self.fields.items()
|
|
||||||
if field.default != fields.UnspecifiedDefault]
|
|
||||||
|
|
||||||
for attr in attrs:
|
|
||||||
default = self.fields[attr].default
|
|
||||||
if default is fields.UnspecifiedDefault:
|
|
||||||
raise exception.ObjectActionError(
|
|
||||||
action='set_defaults',
|
|
||||||
reason='No default set for field %s' % attr)
|
|
||||||
setattr(self, attr, default)
|
|
||||||
|
|
||||||
def obj_load_attr(self, attrname):
|
|
||||||
"""Load an additional attribute from the real object."""
|
|
||||||
raise NotImplementedError(
|
|
||||||
_("Cannot load '%s' in the base class") % attrname)
|
|
||||||
|
|
||||||
def save(self, context):
|
|
||||||
"""Save the changed fields back to the store.
|
|
||||||
|
|
||||||
This is optional for subclasses, but is presented here in the base
|
|
||||||
class for consistency among those that do.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError('Cannot save anything in the base class')
|
|
||||||
|
|
||||||
def obj_what_changed(self):
|
|
||||||
"""Returns a set of fields that have been modified."""
|
|
||||||
changes = set(self._changed_fields)
|
|
||||||
for field in self.fields:
|
|
||||||
if (self.obj_attr_is_set(field) and
|
|
||||||
isinstance(getattr(self, field), CinderObject) and
|
|
||||||
getattr(self, field).obj_what_changed()):
|
|
||||||
changes.add(field)
|
|
||||||
return changes
|
|
||||||
|
|
||||||
def obj_get_changes(self):
|
|
||||||
"""Returns a dict of changed fields and their new values."""
|
|
||||||
changes = {}
|
|
||||||
for key in self.obj_what_changed():
|
|
||||||
changes[key] = getattr(self, key)
|
|
||||||
return changes
|
|
||||||
|
|
||||||
def obj_reset_changes(self, fields=None):
|
|
||||||
"""Reset the list of fields that have been changed.
|
|
||||||
|
|
||||||
Note that this is NOT "revert to previous values"
|
|
||||||
"""
|
|
||||||
if fields:
|
|
||||||
self._changed_fields -= set(fields)
|
|
||||||
else:
|
|
||||||
self._changed_fields.clear()
|
|
||||||
|
|
||||||
def obj_attr_is_set(self, attrname):
|
|
||||||
"""Test object to see if attrname is present.
|
|
||||||
|
|
||||||
Returns True if the named attribute has a value set, or
|
|
||||||
False if not. Raises AttributeError if attrname is not
|
|
||||||
a valid attribute for this object.
|
|
||||||
"""
|
|
||||||
if attrname not in self.obj_fields:
|
|
||||||
raise AttributeError(
|
|
||||||
_("%(objname)s object has no attribute '%(attrname)s'") %
|
|
||||||
{'objname': self.obj_name(), 'attrname': attrname})
|
|
||||||
return hasattr(self, get_attrname(attrname))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def obj_fields(self):
|
|
||||||
return self.fields.keys() + self.obj_extra_fields
|
|
||||||
|
|
||||||
|
|
||||||
class CinderObjectDictCompat(object):
|
|
||||||
"""Mix-in to provide dictionary key access compat
|
|
||||||
|
|
||||||
If an object needs to support attribute access using
|
If an object needs to support attribute access using
|
||||||
dictionary items instead of object attributes, inherit
|
dictionary items instead of object attributes, inherit
|
||||||
@ -568,44 +60,7 @@ class CinderObjectDictCompat(object):
|
|||||||
NOTE(berrange) This class will eventually be deleted.
|
NOTE(berrange) This class will eventually be deleted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# dictish syntactic sugar
|
def get(self, key, value=base._NotSpecifiedSentinel):
|
||||||
def iteritems(self):
|
|
||||||
"""For backwards-compatibility with dict-based objects.
|
|
||||||
|
|
||||||
NOTE(danms): May be removed in the future.
|
|
||||||
"""
|
|
||||||
for name in self.obj_fields:
|
|
||||||
if (self.obj_attr_is_set(name) or
|
|
||||||
name in self.obj_extra_fields):
|
|
||||||
yield name, getattr(self, name)
|
|
||||||
|
|
||||||
items = lambda self: list(self.iteritems())
|
|
||||||
|
|
||||||
def __getitem__(self, name):
|
|
||||||
"""For backwards-compatibility with dict-based objects.
|
|
||||||
|
|
||||||
NOTE(danms): May be removed in the future.
|
|
||||||
"""
|
|
||||||
return getattr(self, name)
|
|
||||||
|
|
||||||
def __setitem__(self, name, value):
|
|
||||||
"""For backwards-compatibility with dict-based objects.
|
|
||||||
|
|
||||||
NOTE(danms): May be removed in the future.
|
|
||||||
"""
|
|
||||||
setattr(self, name, value)
|
|
||||||
|
|
||||||
def __contains__(self, name):
|
|
||||||
"""For backwards-compatibility with dict-based objects.
|
|
||||||
|
|
||||||
NOTE(danms): May be removed in the future.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self.obj_attr_is_set(name)
|
|
||||||
except AttributeError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get(self, key, value=NotSpecifiedSentinel):
|
|
||||||
"""For backwards-compatibility with dict-based objects.
|
"""For backwards-compatibility with dict-based objects.
|
||||||
|
|
||||||
NOTE(danms): May be removed in the future.
|
NOTE(danms): May be removed in the future.
|
||||||
@ -621,19 +76,12 @@ class CinderObjectDictCompat(object):
|
|||||||
{'object_name': self.__class__.__name__,
|
{'object_name': self.__class__.__name__,
|
||||||
'attribute_name': key})
|
'attribute_name': key})
|
||||||
return None
|
return None
|
||||||
if value != NotSpecifiedSentinel and not self.obj_attr_is_set(key):
|
if (value != base._NotSpecifiedSentinel and
|
||||||
|
not self.obj_attr_is_set(key)):
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
return getattr(self, key)
|
return getattr(self, key)
|
||||||
|
|
||||||
def update(self, updates):
|
|
||||||
"""For backwards-compatibility with dict-base objects.
|
|
||||||
|
|
||||||
NOTE(danms): May be removed in the future.
|
|
||||||
"""
|
|
||||||
for key, value in updates.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
|
|
||||||
class CinderPersistentObject(object):
|
class CinderPersistentObject(object):
|
||||||
"""Mixin class for Persistent objects.
|
"""Mixin class for Persistent objects.
|
||||||
@ -656,7 +104,6 @@ class CinderPersistentObject(object):
|
|||||||
|
|
||||||
with obj.obj_as_admin():
|
with obj.obj_as_admin():
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self._context is None:
|
if self._context is None:
|
||||||
raise exception.OrphanedObjectError(method='obj_as_admin',
|
raise exception.OrphanedObjectError(method='obj_as_admin',
|
||||||
@ -670,185 +117,12 @@ class CinderPersistentObject(object):
|
|||||||
self._context = original_context
|
self._context = original_context
|
||||||
|
|
||||||
|
|
||||||
class ObjectListBase(object):
|
class ObjectListBase(base.ObjectListBase):
|
||||||
"""Mixin class for lists of objects.
|
pass
|
||||||
|
|
||||||
This mixin class can be added as a base class for an object that
|
|
||||||
is implementing a list of objects. It adds a single field of 'objects',
|
|
||||||
which is the list store, and behaves like a list itself. It supports
|
|
||||||
serialization of the list of objects automatically.
|
|
||||||
"""
|
|
||||||
fields = {
|
|
||||||
'objects': fields.ListOfObjectsField('CinderObject'),
|
|
||||||
}
|
|
||||||
|
|
||||||
# This is a dictionary of my_version:child_version mappings so that
|
|
||||||
# we can support backleveling our contents based on the version
|
|
||||||
# requested of the list object.
|
|
||||||
child_versions = {}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ObjectListBase, self).__init__(*args, **kwargs)
|
|
||||||
if 'objects' not in kwargs:
|
|
||||||
self.objects = []
|
|
||||||
self._changed_fields.discard('objects')
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""List iterator interface."""
|
|
||||||
return iter(self.objects)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
"""List length."""
|
|
||||||
return len(self.objects)
|
|
||||||
|
|
||||||
def __getitem__(self, index):
|
|
||||||
"""List index access."""
|
|
||||||
if isinstance(index, slice):
|
|
||||||
new_obj = self.__class__()
|
|
||||||
new_obj.objects = self.objects[index]
|
|
||||||
# NOTE(danms): We must be mixed in with a CinderObject!
|
|
||||||
new_obj.obj_reset_changes()
|
|
||||||
new_obj._context = self._context
|
|
||||||
return new_obj
|
|
||||||
return self.objects[index]
|
|
||||||
|
|
||||||
def __contains__(self, value):
|
|
||||||
"""List membership test."""
|
|
||||||
return value in self.objects
|
|
||||||
|
|
||||||
def count(self, value):
|
|
||||||
"""List count of value occurrences."""
|
|
||||||
return self.objects.count(value)
|
|
||||||
|
|
||||||
def index(self, value):
|
|
||||||
"""List index of value."""
|
|
||||||
return self.objects.index(value)
|
|
||||||
|
|
||||||
def sort(self, cmp=None, key=None, reverse=False):
|
|
||||||
self.objects.sort(cmp=cmp, key=key, reverse=reverse)
|
|
||||||
|
|
||||||
def obj_make_compatible(self, primitive, target_version):
|
|
||||||
primitives = primitive['objects']
|
|
||||||
child_target_version = self.child_versions.get(target_version, '1.0')
|
|
||||||
for index, item in enumerate(self.objects):
|
|
||||||
self.objects[index].obj_make_compatible(
|
|
||||||
primitives[index]['cinder_object.data'],
|
|
||||||
child_target_version)
|
|
||||||
primitives[index]['cinder_object.version'] = child_target_version
|
|
||||||
|
|
||||||
def obj_what_changed(self):
|
|
||||||
changes = set(self._changed_fields)
|
|
||||||
for child in self.objects:
|
|
||||||
if child.obj_what_changed():
|
|
||||||
changes.add('objects')
|
|
||||||
return changes
|
|
||||||
|
|
||||||
|
|
||||||
class CinderObjectSerializer(messaging.NoOpSerializer):
|
class CinderObjectSerializer(base.VersionedObjectSerializer):
|
||||||
"""A CinderObject-aware Serializer.
|
OBJ_BASE_CLASS = CinderObject
|
||||||
|
|
||||||
This implements the Oslo Serializer interface and provides the
|
|
||||||
ability to serialize and deserialize CinderObject entities. Any service
|
|
||||||
that needs to accept or return CinderObjects as arguments or result values
|
|
||||||
should pass this to its RPCClient and RPCServer objects.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _process_object(self, context, objprim):
|
|
||||||
try:
|
|
||||||
objinst = CinderObject.obj_from_primitive(objprim, context=context)
|
|
||||||
except exception.IncompatibleObjectVersion:
|
|
||||||
objver = objprim['cinder_object.version']
|
|
||||||
if objver.count('.') == 2:
|
|
||||||
# NOTE(danms): For our purposes, the .z part of the version
|
|
||||||
# should be safe to accept without requiring a backport
|
|
||||||
objprim['cinder_object.version'] = \
|
|
||||||
'.'.join(objver.split('.')[:2])
|
|
||||||
return self._process_object(context, objprim)
|
|
||||||
raise
|
|
||||||
|
|
||||||
return objinst
|
|
||||||
|
|
||||||
def _process_iterable(self, context, action_fn, values):
|
|
||||||
"""Process an iterable, taking an action on each value.
|
|
||||||
:param:context: Request context
|
|
||||||
:param:action_fn: Action to take on each item in values
|
|
||||||
:param:values: Iterable container of things to take action on
|
|
||||||
:returns: A new container of the same type (except set) with
|
|
||||||
items from values having had action applied.
|
|
||||||
"""
|
|
||||||
iterable = values.__class__
|
|
||||||
if issubclass(iterable, dict):
|
|
||||||
return iterable(**{k: action_fn(context, v)
|
|
||||||
for k, v in six.iteritems(values)})
|
|
||||||
else:
|
|
||||||
# NOTE(danms): A set can't have an unhashable value inside, such as
|
|
||||||
# a dict. Convert sets to tuples, which is fine, since we can't
|
|
||||||
# send them over RPC anyway.
|
|
||||||
if iterable == set:
|
|
||||||
iterable = tuple
|
|
||||||
return iterable([action_fn(context, value) for value in values])
|
|
||||||
|
|
||||||
def serialize_entity(self, context, entity):
|
|
||||||
if isinstance(entity, (tuple, list, set, dict)):
|
|
||||||
entity = self._process_iterable(context, self.serialize_entity,
|
|
||||||
entity)
|
|
||||||
elif (hasattr(entity, 'obj_to_primitive') and
|
|
||||||
callable(entity.obj_to_primitive)):
|
|
||||||
entity = entity.obj_to_primitive()
|
|
||||||
return entity
|
|
||||||
|
|
||||||
def deserialize_entity(self, context, entity):
|
|
||||||
if isinstance(entity, dict) and 'cinder_object.name' in entity:
|
|
||||||
entity = self._process_object(context, entity)
|
|
||||||
elif isinstance(entity, (tuple, list, set, dict)):
|
|
||||||
entity = self._process_iterable(context, self.deserialize_entity,
|
|
||||||
entity)
|
|
||||||
return entity
|
|
||||||
|
|
||||||
|
|
||||||
def obj_to_primitive(obj):
|
|
||||||
"""Recursively turn an object into a python primitive.
|
|
||||||
|
|
||||||
A CinderObject becomes a dict, and anything that implements ObjectListBase
|
|
||||||
becomes a list.
|
|
||||||
"""
|
|
||||||
if isinstance(obj, ObjectListBase):
|
|
||||||
return [obj_to_primitive(x) for x in obj]
|
|
||||||
elif isinstance(obj, CinderObject):
|
|
||||||
result = {}
|
|
||||||
for key in obj.obj_fields:
|
|
||||||
if obj.obj_attr_is_set(key) or key in obj.obj_extra_fields:
|
|
||||||
result[key] = obj_to_primitive(getattr(obj, key))
|
|
||||||
return result
|
|
||||||
elif isinstance(obj, netaddr.IPAddress):
|
|
||||||
return str(obj)
|
|
||||||
elif isinstance(obj, netaddr.IPNetwork):
|
|
||||||
return str(obj)
|
|
||||||
else:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
def obj_make_list(context, list_obj, item_cls, db_list, **extra_args):
|
|
||||||
"""Construct an object list from a list of primitives.
|
|
||||||
|
|
||||||
This calls item_cls._from_db_object() on each item of db_list, and
|
|
||||||
adds the resulting object to list_obj.
|
|
||||||
|
|
||||||
:param:context: Request contextr
|
|
||||||
:param:list_obj: An ObjectListBase object
|
|
||||||
:param:item_cls: The CinderObject class of the objects within the list
|
|
||||||
:param:db_list: The list of primitives to convert to objects
|
|
||||||
:param:extra_args: Extra arguments to pass to _from_db_object()
|
|
||||||
:returns: list_obj
|
|
||||||
"""
|
|
||||||
list_obj.objects = []
|
|
||||||
for db_item in db_list:
|
|
||||||
item = item_cls._from_db_object(context, item_cls(), db_item,
|
|
||||||
**extra_args)
|
|
||||||
list_obj.objects.append(item)
|
|
||||||
list_obj._context = context
|
|
||||||
list_obj.obj_reset_changes()
|
|
||||||
return list_obj
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_args(fn):
|
def serialize_args(fn):
|
||||||
|
@ -1,607 +0,0 @@
|
|||||||
# Copyright 2015 Red Hat, Inc.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import iso8601
|
|
||||||
import netaddr
|
|
||||||
from oslo_utils import timeutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
class KeyTypeError(TypeError):
|
|
||||||
def __init__(self, expected, value):
|
|
||||||
super(KeyTypeError, self).__init__(
|
|
||||||
_('Key %(key)s must be of type %(expected)s not %(actual)s'
|
|
||||||
) % {'key': repr(value),
|
|
||||||
'expected': expected.__name__,
|
|
||||||
'actual': value.__class__.__name__,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class ElementTypeError(TypeError):
|
|
||||||
def __init__(self, expected, key, value):
|
|
||||||
super(ElementTypeError, self).__init__(
|
|
||||||
_('Element %(key)s:%(val)s must be of type %(expected)s'
|
|
||||||
' not %(actual)s'
|
|
||||||
) % {'key': key,
|
|
||||||
'val': repr(value),
|
|
||||||
'expected': expected,
|
|
||||||
'actual': value.__class__.__name__,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class AbstractFieldType(object):
|
|
||||||
@abc.abstractmethod
|
|
||||||
def coerce(self, obj, attr, value):
|
|
||||||
"""This is called to coerce (if possible) a value on assignment.
|
|
||||||
|
|
||||||
This method should convert the value given into the designated type,
|
|
||||||
or throw an exception if this is not possible.
|
|
||||||
|
|
||||||
:param:obj: The CinderObject on which an attribute is being set
|
|
||||||
:param:attr: The name of the attribute being set
|
|
||||||
:param:value: The value being set
|
|
||||||
:returns: A properly-typed value
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def from_primitive(self, obj, attr, value):
|
|
||||||
"""This is called to deserialize a value.
|
|
||||||
|
|
||||||
This method should deserialize a value from the form given by
|
|
||||||
to_primitive() to the designated type.
|
|
||||||
|
|
||||||
:param:obj: The CinderObject on which the value is to be set
|
|
||||||
:param:attr: The name of the attribute which will hold the value
|
|
||||||
:param:value: The serialized form of the value
|
|
||||||
:returns: The natural form of the value
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def to_primitive(self, obj, attr, value):
|
|
||||||
"""This is called to serialize a value.
|
|
||||||
|
|
||||||
This method should serialize a value to the form expected by
|
|
||||||
from_primitive().
|
|
||||||
|
|
||||||
:param:obj: The CinderObject on which the value is set
|
|
||||||
:param:attr: The name of the attribute holding the value
|
|
||||||
:param:value: The natural form of the value
|
|
||||||
:returns: The serialized form of the value
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def describe(self):
|
|
||||||
"""Returns a string describing the type of the field."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def stringify(self, value):
|
|
||||||
"""Returns a short stringified version of a value."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FieldType(AbstractFieldType):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_primitive(obj, attr, value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def to_primitive(obj, attr, value):
|
|
||||||
return value
|
|
||||||
|
|
||||||
def describe(self):
|
|
||||||
return self.__class__.__name__
|
|
||||||
|
|
||||||
def stringify(self, value):
|
|
||||||
return str(value)
|
|
||||||
|
|
||||||
|
|
||||||
class UnspecifiedDefault(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Field(object):
|
|
||||||
def __init__(self, field_type, nullable=False,
|
|
||||||
default=UnspecifiedDefault, read_only=False):
|
|
||||||
self._type = field_type
|
|
||||||
self._nullable = nullable
|
|
||||||
self._default = default
|
|
||||||
self._read_only = read_only
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
args = {
|
|
||||||
'nullable': self._nullable,
|
|
||||||
'default': self._default,
|
|
||||||
}
|
|
||||||
return '%s(%s)' % (self._type.__class__.__name__,
|
|
||||||
','.join(['%s=%s' % (k, v)
|
|
||||||
for k, v in args.items()]))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nullable(self):
|
|
||||||
return self._nullable
|
|
||||||
|
|
||||||
@property
|
|
||||||
def default(self):
|
|
||||||
return self._default
|
|
||||||
|
|
||||||
@property
|
|
||||||
def read_only(self):
|
|
||||||
return self._read_only
|
|
||||||
|
|
||||||
def _null(self, obj, attr):
|
|
||||||
if self.nullable:
|
|
||||||
return None
|
|
||||||
elif self._default != UnspecifiedDefault:
|
|
||||||
# NOTE(danms): We coerce the default value each time the field
|
|
||||||
# is set to None as our contract states that we'll let the type
|
|
||||||
# examine the object and attribute name at that time.
|
|
||||||
return self._type.coerce(obj, attr, self._default)
|
|
||||||
else:
|
|
||||||
raise ValueError(_("Field `%s' cannot be None") % attr)
|
|
||||||
|
|
||||||
def coerce(self, obj, attr, value):
|
|
||||||
"""Coerce a value to a suitable type.
|
|
||||||
|
|
||||||
This is called any time you set a value on an object, like:
|
|
||||||
|
|
||||||
foo.myint = 1
|
|
||||||
|
|
||||||
and is responsible for making sure that the value (1 here) is of
|
|
||||||
the proper type, or can be sanely converted.
|
|
||||||
|
|
||||||
This also handles the potentially nullable or defaultable
|
|
||||||
nature of the field and calls the coerce() method on a
|
|
||||||
FieldType to actually do the coercion.
|
|
||||||
|
|
||||||
:param:obj: The object being acted upon
|
|
||||||
:param:attr: The name of the attribute/field being set
|
|
||||||
:param:value: The value being set
|
|
||||||
:returns: The properly-typed value
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return self._null(obj, attr)
|
|
||||||
else:
|
|
||||||
return self._type.coerce(obj, attr, value)
|
|
||||||
|
|
||||||
def from_primitive(self, obj, attr, value):
|
|
||||||
"""Deserialize a value from primitive form.
|
|
||||||
|
|
||||||
This is responsible for deserializing a value from primitive
|
|
||||||
into regular form. It calls the from_primitive() method on a
|
|
||||||
FieldType to do the actual deserialization.
|
|
||||||
|
|
||||||
:param:obj: The object being acted upon
|
|
||||||
:param:attr: The name of the attribute/field being deserialized
|
|
||||||
:param:value: The value to be deserialized
|
|
||||||
:returns: The deserialized value
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return self._type.from_primitive(obj, attr, value)
|
|
||||||
|
|
||||||
def to_primitive(self, obj, attr, value):
|
|
||||||
"""Serialize a value to primitive form.
|
|
||||||
|
|
||||||
This is responsible for serializing a value to primitive
|
|
||||||
form. It calls to_primitive() on a FieldType to do the actual
|
|
||||||
serialization.
|
|
||||||
|
|
||||||
:param:obj: The object being acted upon
|
|
||||||
:param:attr: The name of the attribute/field being serialized
|
|
||||||
:param:value: The value to be serialized
|
|
||||||
:returns: The serialized value
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return self._type.to_primitive(obj, attr, value)
|
|
||||||
|
|
||||||
def describe(self):
|
|
||||||
"""Return a short string describing the type of this field."""
|
|
||||||
name = self._type.describe()
|
|
||||||
prefix = self.nullable and 'Nullable' or ''
|
|
||||||
return prefix + name
|
|
||||||
|
|
||||||
def stringify(self, value):
|
|
||||||
if value is None:
|
|
||||||
return 'None'
|
|
||||||
else:
|
|
||||||
return self._type.stringify(value)
|
|
||||||
|
|
||||||
|
|
||||||
class String(FieldType):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
# FIXME(danms): We should really try to avoid the need to do this
|
|
||||||
if isinstance(value, (six.string_types, int, long, float,
|
|
||||||
datetime.datetime)):
|
|
||||||
return six.text_type(value)
|
|
||||||
else:
|
|
||||||
raise ValueError(_('A string is required here, not %s') %
|
|
||||||
value.__class__.__name__)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def stringify(value):
|
|
||||||
return "'%s'" % value
|
|
||||||
|
|
||||||
|
|
||||||
class UUID(FieldType):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
# FIXME(danms): We should actually verify the UUIDness here
|
|
||||||
return six.text_type(value)
|
|
||||||
|
|
||||||
|
|
||||||
class Integer(FieldType):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
return int(value)
|
|
||||||
|
|
||||||
|
|
||||||
class Float(FieldType):
|
|
||||||
def coerce(self, obj, attr, value):
|
|
||||||
return float(value)
|
|
||||||
|
|
||||||
|
|
||||||
class Boolean(FieldType):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
return bool(value)
|
|
||||||
|
|
||||||
|
|
||||||
class DateTime(FieldType):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
if isinstance(value, six.string_types):
|
|
||||||
# NOTE(danms): Being tolerant of isotime strings here will help us
|
|
||||||
# during our objects transition
|
|
||||||
value = timeutils.parse_isotime(value)
|
|
||||||
elif not isinstance(value, datetime.datetime):
|
|
||||||
raise ValueError(_('A datetime.datetime is required here'))
|
|
||||||
|
|
||||||
if value.utcoffset() is None:
|
|
||||||
# NOTE(danms): Legacy objects from sqlalchemy are stored in UTC,
|
|
||||||
# but are returned without a timezone attached.
|
|
||||||
# As a transitional aid, assume a tz-naive object is in UTC.
|
|
||||||
value = value.replace(tzinfo=iso8601.iso8601.Utc())
|
|
||||||
return value
|
|
||||||
|
|
||||||
def from_primitive(self, obj, attr, value):
|
|
||||||
return self.coerce(obj, attr, timeutils.parse_isotime(value))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def to_primitive(obj, attr, value):
|
|
||||||
return timeutils.isotime(value)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def stringify(value):
|
|
||||||
return timeutils.isotime(value)
|
|
||||||
|
|
||||||
|
|
||||||
class IPAddress(FieldType):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
try:
|
|
||||||
return netaddr.IPAddress(value)
|
|
||||||
except netaddr.AddrFormatError as e:
|
|
||||||
raise ValueError(six.text_type(e))
|
|
||||||
|
|
||||||
def from_primitive(self, obj, attr, value):
|
|
||||||
return self.coerce(obj, attr, value)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def to_primitive(obj, attr, value):
|
|
||||||
return six.text_type(value)
|
|
||||||
|
|
||||||
|
|
||||||
class IPV4Address(IPAddress):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
result = IPAddress.coerce(obj, attr, value)
|
|
||||||
if result.version != 4:
|
|
||||||
raise ValueError(_('Network "%s" is not valid') % value)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class IPV6Address(IPAddress):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
result = IPAddress.coerce(obj, attr, value)
|
|
||||||
if result.version != 6:
|
|
||||||
raise ValueError(_('Network "%s" is not valid') % value)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class IPV4AndV6Address(IPAddress):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
result = IPAddress.coerce(obj, attr, value)
|
|
||||||
if result.version != 4 and result.version != 6:
|
|
||||||
raise ValueError(_('Network "%s" is not valid') % value)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class IPNetwork(IPAddress):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
try:
|
|
||||||
return netaddr.IPNetwork(value)
|
|
||||||
except netaddr.AddrFormatError as e:
|
|
||||||
raise ValueError(six.text_type(e))
|
|
||||||
|
|
||||||
|
|
||||||
class IPV4Network(IPNetwork):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
try:
|
|
||||||
return netaddr.IPNetwork(value, version=4)
|
|
||||||
except netaddr.AddrFormatError as e:
|
|
||||||
raise ValueError(six.text_type(e))
|
|
||||||
|
|
||||||
|
|
||||||
class IPV6Network(IPNetwork):
|
|
||||||
@staticmethod
|
|
||||||
def coerce(obj, attr, value):
|
|
||||||
try:
|
|
||||||
return netaddr.IPNetwork(value, version=6)
|
|
||||||
except netaddr.AddrFormatError as e:
|
|
||||||
raise ValueError(six.text_type(e))
|
|
||||||
|
|
||||||
|
|
||||||
class CompoundFieldType(FieldType):
|
|
||||||
def __init__(self, element_type, **field_args):
|
|
||||||
self._element_type = Field(element_type, **field_args)
|
|
||||||
|
|
||||||
|
|
||||||
class List(CompoundFieldType):
|
|
||||||
def coerce(self, obj, attr, value):
|
|
||||||
if not isinstance(value, list):
|
|
||||||
raise ValueError(_('A list is required here'))
|
|
||||||
for index, element in enumerate(list(value)):
|
|
||||||
value[index] = self._element_type.coerce(
|
|
||||||
obj, '%s[%i]' % (attr, index), element)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def to_primitive(self, obj, attr, value):
|
|
||||||
return [self._element_type.to_primitive(obj, attr, x) for x in value]
|
|
||||||
|
|
||||||
def from_primitive(self, obj, attr, value):
|
|
||||||
return [self._element_type.from_primitive(obj, attr, x) for x in value]
|
|
||||||
|
|
||||||
def stringify(self, value):
|
|
||||||
return '[%s]' % (
|
|
||||||
','.join([self._element_type.stringify(x) for x in value]))
|
|
||||||
|
|
||||||
|
|
||||||
class Dict(CompoundFieldType):
|
|
||||||
def coerce(self, obj, attr, value):
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
raise ValueError(_('A dict is required here'))
|
|
||||||
for key, element in value.items():
|
|
||||||
if not isinstance(key, six.string_types):
|
|
||||||
# NOTE(guohliu) In order to keep compatibility with python3
|
|
||||||
# we need to use six.string_types rather than basestring here,
|
|
||||||
# since six.string_types is a tuple, so we need to pass the
|
|
||||||
# real type in.
|
|
||||||
raise KeyTypeError(six.string_types[0], key)
|
|
||||||
value[key] = self._element_type.coerce(
|
|
||||||
obj, '%s["%s"]' % (attr, key), element)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def to_primitive(self, obj, attr, value):
|
|
||||||
primitive = {}
|
|
||||||
for key, element in value.items():
|
|
||||||
primitive[key] = self._element_type.to_primitive(
|
|
||||||
obj, '%s["%s"]' % (attr, key), element)
|
|
||||||
return primitive
|
|
||||||
|
|
||||||
def from_primitive(self, obj, attr, value):
|
|
||||||
concrete = {}
|
|
||||||
for key, element in value.items():
|
|
||||||
concrete[key] = self._element_type.from_primitive(
|
|
||||||
obj, '%s["%s"]' % (attr, key), element)
|
|
||||||
return concrete
|
|
||||||
|
|
||||||
def stringify(self, value):
|
|
||||||
return '{%s}' % (
|
|
||||||
','.join(['%s=%s' % (key, self._element_type.stringify(val))
|
|
||||||
for key, val in sorted(value.items())]))
|
|
||||||
|
|
||||||
|
|
||||||
class DictProxyField(object):
|
|
||||||
"""Descriptor allowing us to assign pinning data as a dict of key_types.
|
|
||||||
|
|
||||||
This allows us to have an object field that will be a dict of key_type
|
|
||||||
keys, allowing that will convert back to string-keyed dict.
|
|
||||||
|
|
||||||
This will take care of the conversion while the dict field will make sure
|
|
||||||
that we store the raw json-serializable data on the object.
|
|
||||||
|
|
||||||
key_type should return a type that unambiguously responds to six.text_type
|
|
||||||
so that calling key_type on it yields the same thing.
|
|
||||||
"""
|
|
||||||
def __init__(self, dict_field_name, key_type=int):
|
|
||||||
self._fld_name = dict_field_name
|
|
||||||
self._key_type = key_type
|
|
||||||
|
|
||||||
def __get__(self, obj, obj_type=None):
|
|
||||||
if obj is None:
|
|
||||||
return self
|
|
||||||
if getattr(obj, self._fld_name) is None:
|
|
||||||
return
|
|
||||||
return {self._key_type(k): v
|
|
||||||
for k, v in six.iteritems(getattr(obj, self._fld_name))}
|
|
||||||
|
|
||||||
def __set__(self, obj, val):
|
|
||||||
if val is None:
|
|
||||||
setattr(obj, self._fld_name, val)
|
|
||||||
else:
|
|
||||||
setattr(obj, self._fld_name, {six.text_type(k): v
|
|
||||||
for k, v in six.iteritems(val)})
|
|
||||||
|
|
||||||
|
|
||||||
class Set(CompoundFieldType):
|
|
||||||
def coerce(self, obj, attr, value):
|
|
||||||
if not isinstance(value, set):
|
|
||||||
raise ValueError(_('A set is required here'))
|
|
||||||
|
|
||||||
coerced = set()
|
|
||||||
for element in value:
|
|
||||||
coerced.add(self._element_type.coerce(
|
|
||||||
obj, '%s["%s"]' % (attr, element), element))
|
|
||||||
return coerced
|
|
||||||
|
|
||||||
def to_primitive(self, obj, attr, value):
|
|
||||||
return tuple(
|
|
||||||
self._element_type.to_primitive(obj, attr, x) for x in value)
|
|
||||||
|
|
||||||
def from_primitive(self, obj, attr, value):
|
|
||||||
return set([self._element_type.from_primitive(obj, attr, x)
|
|
||||||
for x in value])
|
|
||||||
|
|
||||||
def stringify(self, value):
|
|
||||||
return 'set([%s])' % (
|
|
||||||
','.join([self._element_type.stringify(x) for x in value]))
|
|
||||||
|
|
||||||
|
|
||||||
class Object(FieldType):
|
|
||||||
def __init__(self, obj_name, **kwargs):
|
|
||||||
self._obj_name = obj_name
|
|
||||||
super(Object, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
def coerce(self, obj, attr, value):
|
|
||||||
try:
|
|
||||||
obj_name = value.obj_name()
|
|
||||||
except AttributeError:
|
|
||||||
obj_name = ""
|
|
||||||
|
|
||||||
if obj_name != self._obj_name:
|
|
||||||
raise ValueError(_('An object of type %s is required here') %
|
|
||||||
self._obj_name)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def to_primitive(obj, attr, value):
|
|
||||||
return value.obj_to_primitive()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_primitive(obj, attr, value):
|
|
||||||
# FIXME(danms): Avoid circular import from base.py
|
|
||||||
from cinder.objects import base as obj_base
|
|
||||||
# NOTE (ndipanov): If they already got hydrated by the serializer, just
|
|
||||||
# pass them back unchanged
|
|
||||||
if isinstance(value, obj_base.CinderObject):
|
|
||||||
return value
|
|
||||||
return obj_base.CinderObject.obj_from_primitive(value, obj._context)
|
|
||||||
|
|
||||||
def describe(self):
|
|
||||||
return "Object<%s>" % self._obj_name
|
|
||||||
|
|
||||||
def stringify(self, value):
|
|
||||||
if 'uuid' in value.fields:
|
|
||||||
ident = '(%s)' % (value.obj_attr_is_set('uuid') and value.uuid or
|
|
||||||
'UNKNOWN')
|
|
||||||
elif 'id' in value.fields:
|
|
||||||
ident = '(%s)' % (value.obj_attr_is_set('id') and value.id or
|
|
||||||
'UNKNOWN')
|
|
||||||
else:
|
|
||||||
ident = ''
|
|
||||||
|
|
||||||
return '%s%s' % (self._obj_name, ident)
|
|
||||||
|
|
||||||
|
|
||||||
class AutoTypedField(Field):
|
|
||||||
AUTO_TYPE = None
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super(AutoTypedField, self).__init__(self.AUTO_TYPE, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class StringField(AutoTypedField):
|
|
||||||
AUTO_TYPE = String()
|
|
||||||
|
|
||||||
|
|
||||||
class UUIDField(AutoTypedField):
|
|
||||||
AUTO_TYPE = UUID()
|
|
||||||
|
|
||||||
|
|
||||||
class IntegerField(AutoTypedField):
|
|
||||||
AUTO_TYPE = Integer()
|
|
||||||
|
|
||||||
|
|
||||||
class FloatField(AutoTypedField):
|
|
||||||
AUTO_TYPE = Float()
|
|
||||||
|
|
||||||
|
|
||||||
class BooleanField(AutoTypedField):
|
|
||||||
AUTO_TYPE = Boolean()
|
|
||||||
|
|
||||||
|
|
||||||
class DateTimeField(AutoTypedField):
|
|
||||||
AUTO_TYPE = DateTime()
|
|
||||||
|
|
||||||
|
|
||||||
class DictOfStringsField(AutoTypedField):
|
|
||||||
AUTO_TYPE = Dict(String())
|
|
||||||
|
|
||||||
|
|
||||||
class DictOfNullableStringsField(AutoTypedField):
|
|
||||||
AUTO_TYPE = Dict(String(), nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class DictOfIntegersField(AutoTypedField):
|
|
||||||
AUTO_TYPE = Dict(Integer())
|
|
||||||
|
|
||||||
|
|
||||||
class ListOfStringsField(AutoTypedField):
|
|
||||||
AUTO_TYPE = List(String())
|
|
||||||
|
|
||||||
|
|
||||||
class SetOfIntegersField(AutoTypedField):
|
|
||||||
AUTO_TYPE = Set(Integer())
|
|
||||||
|
|
||||||
|
|
||||||
class ListOfSetsOfIntegersField(AutoTypedField):
|
|
||||||
AUTO_TYPE = List(Set(Integer()))
|
|
||||||
|
|
||||||
|
|
||||||
class ListOfDictOfNullableStringsField(AutoTypedField):
|
|
||||||
AUTO_TYPE = List(Dict(String(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectField(AutoTypedField):
|
|
||||||
def __init__(self, objtype, **kwargs):
|
|
||||||
self.AUTO_TYPE = Object(objtype)
|
|
||||||
super(ObjectField, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ListOfObjectsField(AutoTypedField):
|
|
||||||
def __init__(self, objtype, **kwargs):
|
|
||||||
self.AUTO_TYPE = List(Object(objtype))
|
|
||||||
super(ListOfObjectsField, self).__init__(**kwargs)
|
|
@ -14,13 +14,13 @@
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_versionedobjects import fields
|
||||||
|
|
||||||
from cinder import db
|
from cinder import db
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.objects import base
|
from cinder.objects import base
|
||||||
from cinder.objects import fields
|
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -30,6 +30,7 @@ OPTIONAL_FIELDS = ['volume', 'metadata']
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@base.CinderObjectRegistry.register
|
||||||
class Snapshot(base.CinderPersistentObject, base.CinderObject,
|
class Snapshot(base.CinderPersistentObject, base.CinderObject,
|
||||||
base.CinderObjectDictCompat):
|
base.CinderObjectDictCompat):
|
||||||
# Version 1.0: Initial version
|
# Version 1.0: Initial version
|
||||||
@ -129,7 +130,7 @@ class Snapshot(base.CinderPersistentObject, base.CinderObject,
|
|||||||
expected_attrs=['metadata'])
|
expected_attrs=['metadata'])
|
||||||
|
|
||||||
@base.remotable
|
@base.remotable
|
||||||
def create(self, context):
|
def create(self):
|
||||||
if self.obj_attr_is_set('id'):
|
if self.obj_attr_is_set('id'):
|
||||||
raise exception.ObjectActionError(action='create',
|
raise exception.ObjectActionError(action='create',
|
||||||
reason=_('already created'))
|
reason=_('already created'))
|
||||||
@ -139,11 +140,11 @@ class Snapshot(base.CinderPersistentObject, base.CinderObject,
|
|||||||
raise exception.ObjectActionError(action='create',
|
raise exception.ObjectActionError(action='create',
|
||||||
reason=_('volume assigned'))
|
reason=_('volume assigned'))
|
||||||
|
|
||||||
db_snapshot = db.snapshot_create(context, updates)
|
db_snapshot = db.snapshot_create(self._context, updates)
|
||||||
self._from_db_object(context, self, db_snapshot)
|
self._from_db_object(self._context, self, db_snapshot)
|
||||||
|
|
||||||
@base.remotable
|
@base.remotable
|
||||||
def save(self, context):
|
def save(self):
|
||||||
updates = self.obj_get_changes()
|
updates = self.obj_get_changes()
|
||||||
if updates:
|
if updates:
|
||||||
if 'volume' in updates:
|
if 'volume' in updates:
|
||||||
@ -154,16 +155,17 @@ class Snapshot(base.CinderPersistentObject, base.CinderObject,
|
|||||||
# Metadata items that are not specified in the
|
# Metadata items that are not specified in the
|
||||||
# self.metadata will be deleted
|
# self.metadata will be deleted
|
||||||
metadata = updates.pop('metadata', None)
|
metadata = updates.pop('metadata', None)
|
||||||
self.metadata = db.snapshot_metadata_update(context, self.id,
|
self.metadata = db.snapshot_metadata_update(self._context,
|
||||||
metadata, True)
|
self.id, metadata,
|
||||||
|
True)
|
||||||
|
|
||||||
db.snapshot_update(context, self.id, updates)
|
db.snapshot_update(self._context, self.id, updates)
|
||||||
|
|
||||||
self.obj_reset_changes()
|
self.obj_reset_changes()
|
||||||
|
|
||||||
@base.remotable
|
@base.remotable
|
||||||
def destroy(self, context):
|
def destroy(self):
|
||||||
db.snapshot_destroy(context, self.id)
|
db.snapshot_destroy(self._context, self.id)
|
||||||
|
|
||||||
def obj_load_attr(self, attrname):
|
def obj_load_attr(self, attrname):
|
||||||
if attrname not in OPTIONAL_FIELDS:
|
if attrname not in OPTIONAL_FIELDS:
|
||||||
@ -191,6 +193,7 @@ class Snapshot(base.CinderPersistentObject, base.CinderObject,
|
|||||||
self.obj_reset_changes(['metadata'])
|
self.obj_reset_changes(['metadata'])
|
||||||
|
|
||||||
|
|
||||||
|
@base.CinderObjectRegistry.register
|
||||||
class SnapshotList(base.ObjectListBase, base.CinderObject):
|
class SnapshotList(base.ObjectListBase, base.CinderObject):
|
||||||
VERSION = '1.0'
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
@ -14,13 +14,13 @@
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_versionedobjects import fields
|
||||||
|
|
||||||
from cinder import db
|
from cinder import db
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.objects import base
|
from cinder.objects import base
|
||||||
from cinder.objects import fields
|
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -28,6 +28,7 @@ OPTIONAL_FIELDS = []
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@base.CinderObjectRegistry.register
|
||||||
class Volume(base.CinderPersistentObject, base.CinderObject,
|
class Volume(base.CinderPersistentObject, base.CinderObject,
|
||||||
base.CinderObjectDictCompat):
|
base.CinderObjectDictCompat):
|
||||||
# Version 1.0: Initial version
|
# Version 1.0: Initial version
|
||||||
@ -116,27 +117,28 @@ class Volume(base.CinderPersistentObject, base.CinderObject,
|
|||||||
return cls._from_db_object(context, cls(context), db_volume)
|
return cls._from_db_object(context, cls(context), db_volume)
|
||||||
|
|
||||||
@base.remotable
|
@base.remotable
|
||||||
def create(self, context):
|
def create(self):
|
||||||
if self.obj_attr_is_set('id'):
|
if self.obj_attr_is_set('id'):
|
||||||
raise exception.ObjectActionError(action='create',
|
raise exception.ObjectActionError(action='create',
|
||||||
reason=_('already created'))
|
reason=_('already created'))
|
||||||
updates = self.obj_get_changes()
|
updates = self.obj_get_changes()
|
||||||
db_volume = db.volume_create(context, updates)
|
db_volume = db.volume_create(self._context, updates)
|
||||||
self._from_db_object(context, self, db_volume)
|
self._from_db_object(self._context, self, db_volume)
|
||||||
|
|
||||||
@base.remotable
|
@base.remotable
|
||||||
def save(self, context):
|
def save(self):
|
||||||
updates = self.obj_get_changes()
|
updates = self.obj_get_changes()
|
||||||
if updates:
|
if updates:
|
||||||
db.volume_update(context, self.id, updates)
|
db.volume_update(self._context, self.id, updates)
|
||||||
|
|
||||||
self.obj_reset_changes()
|
self.obj_reset_changes()
|
||||||
|
|
||||||
@base.remotable
|
@base.remotable
|
||||||
def destroy(self, context):
|
def destroy(self):
|
||||||
db.volume_destroy(context, self.id)
|
db.volume_destroy(self._context, self.id)
|
||||||
|
|
||||||
|
|
||||||
|
@base.CinderObjectRegistry.register
|
||||||
class VolumeList(base.ObjectListBase, base.CinderObject):
|
class VolumeList(base.ObjectListBase, base.CinderObject):
|
||||||
VERSION = '1.0'
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
@ -12,8 +12,9 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_versionedobjects import fields
|
||||||
|
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.objects import fields
|
|
||||||
|
|
||||||
|
|
||||||
def fake_db_volume(**updates):
|
def fake_db_volume(**updates):
|
||||||
|
@ -12,8 +12,9 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_versionedobjects import fields
|
||||||
|
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.objects import fields
|
|
||||||
|
|
||||||
|
|
||||||
def fake_db_volume(**updates):
|
def fake_db_volume(**updates):
|
||||||
|
@ -1,316 +0,0 @@
|
|||||||
# Copyright 2015 Red Hat, Inc.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import iso8601
|
|
||||||
from oslo_utils import timeutils
|
|
||||||
|
|
||||||
from cinder.objects import base as obj_base
|
|
||||||
from cinder.objects import fields
|
|
||||||
from cinder import test
|
|
||||||
|
|
||||||
|
|
||||||
class FakeFieldType(fields.FieldType):
|
|
||||||
def coerce(self, obj, attr, value):
|
|
||||||
return '*%s*' % value
|
|
||||||
|
|
||||||
def to_primitive(self, obj, attr, value):
|
|
||||||
return '!%s!' % value
|
|
||||||
|
|
||||||
def from_primitive(self, obj, attr, value):
|
|
||||||
return value[1:-1]
|
|
||||||
|
|
||||||
|
|
||||||
class TestField(test.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestField, self).setUp()
|
|
||||||
self.field = fields.Field(FakeFieldType())
|
|
||||||
self.coerce_good_values = [('foo', '*foo*')]
|
|
||||||
self.coerce_bad_values = []
|
|
||||||
self.to_primitive_values = [('foo', '!foo!')]
|
|
||||||
self.from_primitive_values = [('!foo!', 'foo')]
|
|
||||||
|
|
||||||
def test_coerce_good_values(self):
|
|
||||||
for in_val, out_val in self.coerce_good_values:
|
|
||||||
self.assertEqual(out_val, self.field.coerce('obj', 'attr', in_val))
|
|
||||||
|
|
||||||
def test_coerce_bad_values(self):
|
|
||||||
for in_val in self.coerce_bad_values:
|
|
||||||
self.assertRaises((TypeError, ValueError),
|
|
||||||
self.field.coerce, 'obj', 'attr', in_val)
|
|
||||||
|
|
||||||
def test_to_primitive(self):
|
|
||||||
for in_val, prim_val in self.to_primitive_values:
|
|
||||||
self.assertEqual(prim_val, self.field.to_primitive('obj', 'attr',
|
|
||||||
in_val))
|
|
||||||
|
|
||||||
def test_from_primitive(self):
|
|
||||||
class ObjectLikeThing(object):
|
|
||||||
_context = 'context'
|
|
||||||
|
|
||||||
for prim_val, out_val in self.from_primitive_values:
|
|
||||||
self.assertEqual(out_val, self.field.from_primitive(
|
|
||||||
ObjectLikeThing, 'attr', prim_val))
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual('123', self.field.stringify(123))
|
|
||||||
|
|
||||||
|
|
||||||
class TestString(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestField, self).setUp()
|
|
||||||
self.field = fields.StringField()
|
|
||||||
self.coerce_good_values = [('foo', 'foo'), (1, '1'), (1L, '1'),
|
|
||||||
(True, 'True')]
|
|
||||||
self.coerce_bad_values = [None]
|
|
||||||
self.to_primitive_values = self.coerce_good_values[0:1]
|
|
||||||
self.from_primitive_values = self.coerce_good_values[0:1]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual("'123'", self.field.stringify(123))
|
|
||||||
|
|
||||||
|
|
||||||
class TestInteger(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestField, self).setUp()
|
|
||||||
self.field = fields.IntegerField()
|
|
||||||
self.coerce_good_values = [(1, 1), ('1', 1)]
|
|
||||||
self.coerce_bad_values = ['foo', None]
|
|
||||||
self.to_primitive_values = self.coerce_good_values[0:1]
|
|
||||||
self.from_primitive_values = self.coerce_good_values[0:1]
|
|
||||||
|
|
||||||
|
|
||||||
class TestFloat(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestFloat, self).setUp()
|
|
||||||
self.field = fields.FloatField()
|
|
||||||
self.coerce_good_values = [(1.1, 1.1), ('1.1', 1.1)]
|
|
||||||
self.coerce_bad_values = ['foo', None]
|
|
||||||
self.to_primitive_values = self.coerce_good_values[0:1]
|
|
||||||
self.from_primitive_values = self.coerce_good_values[0:1]
|
|
||||||
|
|
||||||
|
|
||||||
class TestBoolean(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestField, self).setUp()
|
|
||||||
self.field = fields.BooleanField()
|
|
||||||
self.coerce_good_values = [(True, True), (False, False), (1, True),
|
|
||||||
('foo', True), (0, False), ('', False)]
|
|
||||||
self.coerce_bad_values = []
|
|
||||||
self.to_primitive_values = self.coerce_good_values[0:2]
|
|
||||||
self.from_primitive_values = self.coerce_good_values[0:2]
|
|
||||||
|
|
||||||
|
|
||||||
class TestDateTime(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestDateTime, self).setUp()
|
|
||||||
self.dt = datetime.datetime(1955, 11, 5, tzinfo=iso8601.iso8601.Utc())
|
|
||||||
self.field = fields.DateTimeField()
|
|
||||||
self.coerce_good_values = [(self.dt, self.dt),
|
|
||||||
(timeutils.isotime(self.dt), self.dt)]
|
|
||||||
self.coerce_bad_values = [1, 'foo']
|
|
||||||
self.to_primitive_values = [(self.dt, timeutils.isotime(self.dt))]
|
|
||||||
self.from_primitive_values = [(timeutils.isotime(self.dt), self.dt)]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual(
|
|
||||||
'1955-11-05T18:00:00Z',
|
|
||||||
self.field.stringify(
|
|
||||||
datetime.datetime(1955, 11, 5, 18, 0, 0,
|
|
||||||
tzinfo=iso8601.iso8601.Utc())))
|
|
||||||
|
|
||||||
|
|
||||||
class TestDict(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestDict, self).setUp()
|
|
||||||
self.field = fields.Field(fields.Dict(FakeFieldType()))
|
|
||||||
self.coerce_good_values = [({'foo': 'bar'}, {'foo': '*bar*'}),
|
|
||||||
({'foo': 1}, {'foo': '*1*'})]
|
|
||||||
self.coerce_bad_values = [{1: 'bar'}, 'foo']
|
|
||||||
self.to_primitive_values = [({'foo': 'bar'}, {'foo': '!bar!'})]
|
|
||||||
self.from_primitive_values = [({'foo': '!bar!'}, {'foo': 'bar'})]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual("{key=val}", self.field.stringify({'key': 'val'}))
|
|
||||||
|
|
||||||
|
|
||||||
class TestDictOfStrings(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestDictOfStrings, self).setUp()
|
|
||||||
self.field = fields.DictOfStringsField()
|
|
||||||
self.coerce_good_values = [({'foo': 'bar'}, {'foo': 'bar'}),
|
|
||||||
({'foo': 1}, {'foo': '1'})]
|
|
||||||
self.coerce_bad_values = [{1: 'bar'}, {'foo': None}, 'foo']
|
|
||||||
self.to_primitive_values = [({'foo': 'bar'}, {'foo': 'bar'})]
|
|
||||||
self.from_primitive_values = [({'foo': 'bar'}, {'foo': 'bar'})]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual("{key='val'}", self.field.stringify({'key': 'val'}))
|
|
||||||
|
|
||||||
|
|
||||||
class TestDictOfIntegers(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestDictOfIntegers, self).setUp()
|
|
||||||
self.field = fields.DictOfIntegersField()
|
|
||||||
self.coerce_good_values = [({'foo': '42'}, {'foo': 42}),
|
|
||||||
({'foo': 4.2}, {'foo': 4})]
|
|
||||||
self.coerce_bad_values = [{1: 'bar'}, {'foo': 'boo'},
|
|
||||||
'foo', {'foo': None}]
|
|
||||||
self.to_primitive_values = [({'foo': 42}, {'foo': 42})]
|
|
||||||
self.from_primitive_values = [({'foo': 42}, {'foo': 42})]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual("{key=42}", self.field.stringify({'key': 42}))
|
|
||||||
|
|
||||||
|
|
||||||
class TestDictOfStringsNone(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestDictOfStringsNone, self).setUp()
|
|
||||||
self.field = fields.DictOfNullableStringsField()
|
|
||||||
self.coerce_good_values = [({'foo': 'bar'}, {'foo': 'bar'}),
|
|
||||||
({'foo': 1}, {'foo': '1'}),
|
|
||||||
({'foo': None}, {'foo': None})]
|
|
||||||
self.coerce_bad_values = [{1: 'bar'}, 'foo']
|
|
||||||
self.to_primitive_values = [({'foo': 'bar'}, {'foo': 'bar'})]
|
|
||||||
self.from_primitive_values = [({'foo': 'bar'}, {'foo': 'bar'})]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual("{k2=None,key='val'}",
|
|
||||||
self.field.stringify({'k2': None,
|
|
||||||
'key': 'val'}))
|
|
||||||
|
|
||||||
|
|
||||||
class TestListOfDictOfNullableStringsField(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestListOfDictOfNullableStringsField, self).setUp()
|
|
||||||
self.field = fields.ListOfDictOfNullableStringsField()
|
|
||||||
self.coerce_good_values = [([{'f': 'b', 'f1': 'b1'}, {'f2': 'b2'}],
|
|
||||||
[{'f': 'b', 'f1': 'b1'}, {'f2': 'b2'}]),
|
|
||||||
([{'f': 1}, {'f1': 'b1'}],
|
|
||||||
[{'f': '1'}, {'f1': 'b1'}]),
|
|
||||||
([{'foo': None}], [{'foo': None}])]
|
|
||||||
self.coerce_bad_values = [[{1: 'a'}], ['ham', 1], ['eggs']]
|
|
||||||
self.to_primitive_values = [([{'f': 'b'}, {'f1': 'b1'}, {'f2': None}],
|
|
||||||
[{'f': 'b'}, {'f1': 'b1'}, {'f2': None}])]
|
|
||||||
self.from_primitive_values = [([{'f': 'b'}, {'f1': 'b1'},
|
|
||||||
{'f2': None}],
|
|
||||||
[{'f': 'b'}, {'f1': 'b1'},
|
|
||||||
{'f2': None}])]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual("[{f=None,f1='b1'},{f2='b2'}]",
|
|
||||||
self.field.stringify(
|
|
||||||
[{'f': None, 'f1': 'b1'}, {'f2': 'b2'}]))
|
|
||||||
|
|
||||||
|
|
||||||
class TestList(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestList, self).setUp()
|
|
||||||
self.field = fields.Field(fields.List(FakeFieldType()))
|
|
||||||
self.coerce_good_values = [(['foo', 'bar'], ['*foo*', '*bar*'])]
|
|
||||||
self.coerce_bad_values = ['foo']
|
|
||||||
self.to_primitive_values = [(['foo'], ['!foo!'])]
|
|
||||||
self.from_primitive_values = [(['!foo!'], ['foo'])]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual('[123]', self.field.stringify([123]))
|
|
||||||
|
|
||||||
|
|
||||||
class TestListOfStrings(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestListOfStrings, self).setUp()
|
|
||||||
self.field = fields.ListOfStringsField()
|
|
||||||
self.coerce_good_values = [(['foo', 'bar'], ['foo', 'bar'])]
|
|
||||||
self.coerce_bad_values = ['foo']
|
|
||||||
self.to_primitive_values = [(['foo'], ['foo'])]
|
|
||||||
self.from_primitive_values = [(['foo'], ['foo'])]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual("['abc']", self.field.stringify(['abc']))
|
|
||||||
|
|
||||||
|
|
||||||
class TestSet(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestSet, self).setUp()
|
|
||||||
self.field = fields.Field(fields.Set(FakeFieldType()))
|
|
||||||
self.coerce_good_values = [(set(['foo', 'bar']),
|
|
||||||
set(['*foo*', '*bar*']))]
|
|
||||||
self.coerce_bad_values = [['foo'], {'foo': 'bar'}]
|
|
||||||
self.to_primitive_values = [(set(['foo']), tuple(['!foo!']))]
|
|
||||||
self.from_primitive_values = [(tuple(['!foo!']), set(['foo']))]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual('set([123])', self.field.stringify(set([123])))
|
|
||||||
|
|
||||||
|
|
||||||
class TestSetOfIntegers(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestSetOfIntegers, self).setUp()
|
|
||||||
self.field = fields.SetOfIntegersField()
|
|
||||||
self.coerce_good_values = [(set(['1', 2]),
|
|
||||||
set([1, 2]))]
|
|
||||||
self.coerce_bad_values = [set(['foo'])]
|
|
||||||
self.to_primitive_values = [(set([1]), tuple([1]))]
|
|
||||||
self.from_primitive_values = [(tuple([1]), set([1]))]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual('set([1,2])', self.field.stringify(set([1, 2])))
|
|
||||||
|
|
||||||
|
|
||||||
class TestListOfSetsOfIntegers(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestListOfSetsOfIntegers, self).setUp()
|
|
||||||
self.field = fields.ListOfSetsOfIntegersField()
|
|
||||||
self.coerce_good_values = [([set(['1', 2]), set([3, '4'])],
|
|
||||||
[set([1, 2]), set([3, 4])])]
|
|
||||||
self.coerce_bad_values = [[set(['foo'])]]
|
|
||||||
self.to_primitive_values = [([set([1])], [tuple([1])])]
|
|
||||||
self.from_primitive_values = [([tuple([1])], [set([1])])]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
self.assertEqual('[set([1,2])]', self.field.stringify([set([1, 2])]))
|
|
||||||
|
|
||||||
|
|
||||||
class TestObject(TestField):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestObject, self).setUp()
|
|
||||||
|
|
||||||
class TestableObject(obj_base.CinderObject):
|
|
||||||
fields = {
|
|
||||||
'uuid': fields.StringField(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __eq__(self, value):
|
|
||||||
# NOTE(danms): Be rather lax about this equality thing to
|
|
||||||
# satisfy the assertEqual() in test_from_primitive(). We
|
|
||||||
# just want to make sure the right type of object is re-created
|
|
||||||
return value.__class__.__name__ == TestableObject.__name__
|
|
||||||
|
|
||||||
class OtherTestableObject(obj_base.CinderObject):
|
|
||||||
pass
|
|
||||||
|
|
||||||
test_inst = TestableObject()
|
|
||||||
self._test_cls = TestableObject
|
|
||||||
self.field = fields.Field(fields.Object('TestableObject'))
|
|
||||||
self.coerce_good_values = [(test_inst, test_inst)]
|
|
||||||
self.coerce_bad_values = [OtherTestableObject(), 1, 'foo']
|
|
||||||
self.to_primitive_values = [(test_inst, test_inst.obj_to_primitive())]
|
|
||||||
self.from_primitive_values = [(test_inst.obj_to_primitive(),
|
|
||||||
test_inst), (test_inst, test_inst)]
|
|
||||||
|
|
||||||
def test_stringify(self):
|
|
||||||
obj = self._test_cls(uuid='fake-uuid')
|
|
||||||
self.assertEqual('TestableObject(fake-uuid)',
|
|
||||||
self.field.stringify(obj))
|
|
@ -1,966 +0,0 @@
|
|||||||
# Copyright 2015 IBM Corp.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import copy
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import mock
|
|
||||||
from oslo_serialization import jsonutils
|
|
||||||
from oslo_utils import timeutils
|
|
||||||
import six
|
|
||||||
from testtools import matchers
|
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import objects
|
|
||||||
from cinder.objects import base
|
|
||||||
from cinder.objects import fields
|
|
||||||
from cinder import test
|
|
||||||
from cinder.tests.unit import fake_notifier
|
|
||||||
|
|
||||||
|
|
||||||
class MyOwnedObject(base.CinderPersistentObject, base.CinderObject):
|
|
||||||
VERSION = '1.0'
|
|
||||||
fields = {'baz': fields.Field(fields.Integer())}
|
|
||||||
|
|
||||||
|
|
||||||
class MyObj(base.CinderPersistentObject, base.CinderObject,
|
|
||||||
base.CinderObjectDictCompat):
|
|
||||||
VERSION = '1.6'
|
|
||||||
fields = {'foo': fields.Field(fields.Integer(), default=1),
|
|
||||||
'bar': fields.Field(fields.String()),
|
|
||||||
'missing': fields.Field(fields.String()),
|
|
||||||
'readonly': fields.Field(fields.Integer(), read_only=True),
|
|
||||||
'rel_object': fields.ObjectField('MyOwnedObject', nullable=True),
|
|
||||||
'rel_objects': fields.ListOfObjectsField('MyOwnedObject',
|
|
||||||
nullable=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _from_db_object(context, obj, db_obj):
|
|
||||||
self = MyObj()
|
|
||||||
self.foo = db_obj['foo']
|
|
||||||
self.bar = db_obj['bar']
|
|
||||||
self.missing = db_obj['missing']
|
|
||||||
self.readonly = 1
|
|
||||||
return self
|
|
||||||
|
|
||||||
def obj_load_attr(self, attrname):
|
|
||||||
setattr(self, attrname, 'loaded!')
|
|
||||||
|
|
||||||
@base.remotable_classmethod
|
|
||||||
def query(cls, context):
|
|
||||||
obj = cls(context=context, foo=1, bar='bar')
|
|
||||||
obj.obj_reset_changes()
|
|
||||||
return obj
|
|
||||||
|
|
||||||
@base.remotable
|
|
||||||
def marco(self, context):
|
|
||||||
return 'polo'
|
|
||||||
|
|
||||||
@base.remotable
|
|
||||||
def _update_test(self, context):
|
|
||||||
if context.project_id == 'alternate':
|
|
||||||
self.bar = 'alternate-context'
|
|
||||||
else:
|
|
||||||
self.bar = 'updated'
|
|
||||||
|
|
||||||
@base.remotable
|
|
||||||
def save(self, context):
|
|
||||||
self.obj_reset_changes()
|
|
||||||
|
|
||||||
@base.remotable
|
|
||||||
def refresh(self, context):
|
|
||||||
self.foo = 321
|
|
||||||
self.bar = 'refreshed'
|
|
||||||
self.obj_reset_changes()
|
|
||||||
|
|
||||||
@base.remotable
|
|
||||||
def modify_save_modify(self, context):
|
|
||||||
self.bar = 'meow'
|
|
||||||
self.save()
|
|
||||||
self.foo = 42
|
|
||||||
self.rel_object = MyOwnedObject(baz=42)
|
|
||||||
|
|
||||||
def obj_make_compatible(self, primitive, target_version):
|
|
||||||
super(MyObj, self).obj_make_compatible(primitive, target_version)
|
|
||||||
# NOTE(danms): Simulate an older version that had a different
|
|
||||||
# format for the 'bar' attribute
|
|
||||||
if target_version == '1.1' and 'bar' in primitive:
|
|
||||||
primitive['bar'] = 'old%s' % primitive['bar']
|
|
||||||
|
|
||||||
|
|
||||||
class MyObjDiffVers(MyObj):
|
|
||||||
VERSION = '1.5'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def obj_name(cls):
|
|
||||||
return 'MyObj'
|
|
||||||
|
|
||||||
|
|
||||||
class MyObj2(object):
|
|
||||||
@classmethod
|
|
||||||
def obj_name(cls):
|
|
||||||
return 'MyObj'
|
|
||||||
|
|
||||||
@base.remotable_classmethod
|
|
||||||
def query(cls, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RandomMixInWithNoFields(object):
|
|
||||||
"""Used to test object inheritance using a mixin that has no fields."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestSubclassedObject(RandomMixInWithNoFields, MyObj):
|
|
||||||
fields = {'new_field': fields.Field(fields.String())}
|
|
||||||
|
|
||||||
|
|
||||||
class TestMetaclass(test.TestCase):
|
|
||||||
def test_obj_tracking(self):
|
|
||||||
|
|
||||||
@six.add_metaclass(base.CinderObjectMetaclass)
|
|
||||||
class NewBaseClass(object):
|
|
||||||
VERSION = '1.0'
|
|
||||||
fields = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def obj_name(cls):
|
|
||||||
return cls.__name__
|
|
||||||
|
|
||||||
class Fake1TestObj1(NewBaseClass):
|
|
||||||
@classmethod
|
|
||||||
def obj_name(cls):
|
|
||||||
return 'fake1'
|
|
||||||
|
|
||||||
class Fake1TestObj2(Fake1TestObj1):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Fake1TestObj3(Fake1TestObj1):
|
|
||||||
VERSION = '1.1'
|
|
||||||
|
|
||||||
class Fake2TestObj1(NewBaseClass):
|
|
||||||
@classmethod
|
|
||||||
def obj_name(cls):
|
|
||||||
return 'fake2'
|
|
||||||
|
|
||||||
class Fake1TestObj4(Fake1TestObj3):
|
|
||||||
VERSION = '1.2'
|
|
||||||
|
|
||||||
class Fake2TestObj2(Fake2TestObj1):
|
|
||||||
VERSION = '1.1'
|
|
||||||
|
|
||||||
class Fake1TestObj5(Fake1TestObj1):
|
|
||||||
VERSION = '1.1'
|
|
||||||
|
|
||||||
# Newest versions first in the list. Duplicate versions take the
|
|
||||||
# newest object.
|
|
||||||
expected = {'fake1': [Fake1TestObj4, Fake1TestObj5, Fake1TestObj2],
|
|
||||||
'fake2': [Fake2TestObj2, Fake2TestObj1]}
|
|
||||||
self.assertEqual(expected, NewBaseClass._obj_classes)
|
|
||||||
# The following should work, also.
|
|
||||||
self.assertEqual(expected, Fake1TestObj1._obj_classes)
|
|
||||||
self.assertEqual(expected, Fake1TestObj2._obj_classes)
|
|
||||||
self.assertEqual(expected, Fake1TestObj3._obj_classes)
|
|
||||||
self.assertEqual(expected, Fake1TestObj4._obj_classes)
|
|
||||||
self.assertEqual(expected, Fake1TestObj5._obj_classes)
|
|
||||||
self.assertEqual(expected, Fake2TestObj1._obj_classes)
|
|
||||||
self.assertEqual(expected, Fake2TestObj2._obj_classes)
|
|
||||||
|
|
||||||
def test_field_checking(self):
|
|
||||||
def create_class(field):
|
|
||||||
class TestField(base.CinderObject):
|
|
||||||
VERSION = '1.5'
|
|
||||||
fields = {'foo': field()}
|
|
||||||
return TestField
|
|
||||||
|
|
||||||
create_class(fields.BooleanField)
|
|
||||||
self.assertRaises(exception.ObjectFieldInvalid,
|
|
||||||
create_class, fields.Boolean)
|
|
||||||
self.assertRaises(exception.ObjectFieldInvalid,
|
|
||||||
create_class, int)
|
|
||||||
|
|
||||||
|
|
||||||
class TestObjToPrimitive(test.TestCase):
|
|
||||||
|
|
||||||
def test_obj_to_primitive_list(self):
|
|
||||||
class MyObjElement(base.CinderObject):
|
|
||||||
fields = {'foo': fields.IntegerField()}
|
|
||||||
|
|
||||||
def __init__(self, foo):
|
|
||||||
super(MyObjElement, self).__init__()
|
|
||||||
self.foo = foo
|
|
||||||
|
|
||||||
class MyList(base.ObjectListBase, base.CinderObject):
|
|
||||||
fields = {'objects': fields.ListOfObjectsField('MyObjElement')}
|
|
||||||
|
|
||||||
mylist = MyList()
|
|
||||||
mylist.objects = [MyObjElement(1), MyObjElement(2), MyObjElement(3)]
|
|
||||||
self.assertEqual([1, 2, 3],
|
|
||||||
[x['foo'] for x in base.obj_to_primitive(mylist)])
|
|
||||||
|
|
||||||
def test_obj_to_primitive_dict(self):
|
|
||||||
myobj = MyObj(foo=1, bar='foo')
|
|
||||||
self.assertEqual({'foo': 1, 'bar': 'foo'},
|
|
||||||
base.obj_to_primitive(myobj))
|
|
||||||
|
|
||||||
def test_obj_to_primitive_recursive(self):
|
|
||||||
class MyList(base.ObjectListBase, base.CinderObject):
|
|
||||||
fields = {'objects': fields.ListOfObjectsField('MyObj')}
|
|
||||||
|
|
||||||
mylist = MyList(objects=[MyObj(), MyObj()])
|
|
||||||
for i, value in enumerate(mylist):
|
|
||||||
value.foo = i
|
|
||||||
self.assertEqual([{'foo': 0}, {'foo': 1}],
|
|
||||||
base.obj_to_primitive(mylist))
|
|
||||||
|
|
||||||
|
|
||||||
class TestObjMakeList(test.TestCase):
|
|
||||||
|
|
||||||
def test_obj_make_list(self):
|
|
||||||
class MyList(base.ObjectListBase, base.CinderObject):
|
|
||||||
pass
|
|
||||||
|
|
||||||
db_objs = [{'foo': 1, 'bar': 'baz', 'missing': 'banana'},
|
|
||||||
{'foo': 2, 'bar': 'bat', 'missing': 'apple'},
|
|
||||||
]
|
|
||||||
mylist = base.obj_make_list('ctxt', MyList(), MyObj, db_objs)
|
|
||||||
self.assertEqual(2, len(mylist))
|
|
||||||
self.assertEqual('ctxt', mylist._context)
|
|
||||||
for index, item in enumerate(mylist):
|
|
||||||
self.assertEqual(db_objs[index]['foo'], item.foo)
|
|
||||||
self.assertEqual(db_objs[index]['bar'], item.bar)
|
|
||||||
self.assertEqual(db_objs[index]['missing'], item.missing)
|
|
||||||
|
|
||||||
|
|
||||||
def compare_obj(test, obj, db_obj, subs=None, allow_missing=None,
|
|
||||||
comparators=None):
|
|
||||||
"""Compare a CinderObject and a dict-like database object.
|
|
||||||
|
|
||||||
This automatically converts TZ-aware datetimes and iterates over
|
|
||||||
the fields of the object.
|
|
||||||
|
|
||||||
:param:test: The TestCase doing the comparison
|
|
||||||
:param:obj: The CinderObject to examine
|
|
||||||
:param:db_obj: The dict-like database object to use as reference
|
|
||||||
:param:subs: A dict of objkey=dbkey field substitutions
|
|
||||||
:param:allow_missing: A list of fields that may not be in db_obj
|
|
||||||
:param:comparators: Map of comparator functions to use for certain fields
|
|
||||||
"""
|
|
||||||
|
|
||||||
if subs is None:
|
|
||||||
subs = {}
|
|
||||||
if allow_missing is None:
|
|
||||||
allow_missing = []
|
|
||||||
if comparators is None:
|
|
||||||
comparators = {}
|
|
||||||
|
|
||||||
for key in obj.fields:
|
|
||||||
if key in allow_missing and not obj.obj_attr_is_set(key):
|
|
||||||
continue
|
|
||||||
obj_val = getattr(obj, key)
|
|
||||||
db_key = subs.get(key, key)
|
|
||||||
db_val = db_obj[db_key]
|
|
||||||
if isinstance(obj_val, datetime.datetime):
|
|
||||||
obj_val = obj_val.replace(tzinfo=None)
|
|
||||||
|
|
||||||
if key in comparators:
|
|
||||||
comparator = comparators[key]
|
|
||||||
comparator(db_val, obj_val)
|
|
||||||
else:
|
|
||||||
test.assertEqual(db_val, obj_val)
|
|
||||||
|
|
||||||
|
|
||||||
class _BaseTestCase(test.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(_BaseTestCase, self).setUp()
|
|
||||||
self.remote_object_calls = list()
|
|
||||||
self.user_id = 'fake-user'
|
|
||||||
self.project_id = 'fake-project'
|
|
||||||
self.context = context.RequestContext(self.user_id, self.project_id)
|
|
||||||
fake_notifier.stub_notifier(self.stubs)
|
|
||||||
self.addCleanup(fake_notifier.reset)
|
|
||||||
|
|
||||||
def compare_obj(self, obj, db_obj, subs=None, allow_missing=None,
|
|
||||||
comparators=None):
|
|
||||||
compare_obj(self, obj, db_obj, subs=subs, allow_missing=allow_missing,
|
|
||||||
comparators=comparators)
|
|
||||||
|
|
||||||
def json_comparator(self, expected, obj_val):
|
|
||||||
# json-ify an object field for comparison with its db str
|
|
||||||
# equivalent
|
|
||||||
self.assertEqual(expected, jsonutils.dumps(obj_val))
|
|
||||||
|
|
||||||
def str_comparator(self, expected, obj_val):
|
|
||||||
"""Compare an object field to a string in the db by performing
|
|
||||||
a simple coercion on the object field value.
|
|
||||||
"""
|
|
||||||
self.assertEqual(expected, str(obj_val))
|
|
||||||
|
|
||||||
def assertNotIsInstance(self, obj, cls, msg=None):
|
|
||||||
"""Python < v2.7 compatibility. Assert 'not isinstance(obj, cls)."""
|
|
||||||
try:
|
|
||||||
f = super(_BaseTestCase, self).assertNotIsInstance
|
|
||||||
except AttributeError:
|
|
||||||
self.assertThat(obj,
|
|
||||||
matchers.Not(matchers.IsInstance(cls)),
|
|
||||||
message=msg or '')
|
|
||||||
else:
|
|
||||||
f(obj, cls, msg=msg)
|
|
||||||
|
|
||||||
|
|
||||||
class _LocalTest(_BaseTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(_LocalTest, self).setUp()
|
|
||||||
# Just in case
|
|
||||||
base.CinderObject.indirection_api = None
|
|
||||||
|
|
||||||
def assertRemotes(self):
|
|
||||||
self.assertEqual(self.remote_object_calls, [])
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def things_temporarily_local():
|
|
||||||
_api = base.CinderObject.indirection_api
|
|
||||||
base.CinderObject.indirection_api = None
|
|
||||||
yield
|
|
||||||
base.CinderObject.indirection_api = _api
|
|
||||||
|
|
||||||
|
|
||||||
class _TestObject(object):
|
|
||||||
def test_object_attrs_in_init(self):
|
|
||||||
# Now check the test one in this file. Should be newest version
|
|
||||||
self.assertEqual('1.6', objects.MyObj.VERSION)
|
|
||||||
|
|
||||||
def test_hydration_type_error(self):
|
|
||||||
primitive = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': '1.5',
|
|
||||||
'cinder_object.data': {'foo': 'a'}}
|
|
||||||
self.assertRaises(ValueError, MyObj.obj_from_primitive, primitive)
|
|
||||||
|
|
||||||
def test_hydration(self):
|
|
||||||
primitive = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': '1.5',
|
|
||||||
'cinder_object.data': {'foo': 1}}
|
|
||||||
real_method = MyObj._obj_from_primitive
|
|
||||||
|
|
||||||
def _obj_from_primitive(*args):
|
|
||||||
return real_method(*args)
|
|
||||||
|
|
||||||
with mock.patch.object(MyObj, '_obj_from_primitive') as ofp:
|
|
||||||
ofp.side_effect = _obj_from_primitive
|
|
||||||
obj = MyObj.obj_from_primitive(primitive)
|
|
||||||
ofp.assert_called_once_with(None, '1.5', primitive)
|
|
||||||
self.assertEqual(obj.foo, 1)
|
|
||||||
|
|
||||||
def test_hydration_version_different(self):
|
|
||||||
primitive = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': '1.2',
|
|
||||||
'cinder_object.data': {'foo': 1}}
|
|
||||||
obj = MyObj.obj_from_primitive(primitive)
|
|
||||||
self.assertEqual(obj.foo, 1)
|
|
||||||
self.assertEqual('1.2', obj.VERSION)
|
|
||||||
|
|
||||||
def test_hydration_bad_ns(self):
|
|
||||||
primitive = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'foo',
|
|
||||||
'cinder_object.version': '1.5',
|
|
||||||
'cinder_object.data': {'foo': 1}}
|
|
||||||
self.assertRaises(exception.UnsupportedObjectError,
|
|
||||||
MyObj.obj_from_primitive, primitive)
|
|
||||||
|
|
||||||
def test_hydration_additional_unexpected_stuff(self):
|
|
||||||
primitive = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': '1.5.1',
|
|
||||||
'cinder_object.data': {
|
|
||||||
'foo': 1,
|
|
||||||
'unexpected_thing': 'foobar'}}
|
|
||||||
obj = MyObj.obj_from_primitive(primitive)
|
|
||||||
self.assertEqual(1, obj.foo)
|
|
||||||
self.assertFalse(hasattr(obj, 'unexpected_thing'))
|
|
||||||
# NOTE(danms): If we call obj_from_primitive() directly
|
|
||||||
# with a version containing .z, we'll get that version
|
|
||||||
# in the resulting object. In reality, when using the
|
|
||||||
# serializer, we'll get that snipped off (tested
|
|
||||||
# elsewhere)
|
|
||||||
self.assertEqual('1.5.1', obj.VERSION)
|
|
||||||
|
|
||||||
def test_dehydration(self):
|
|
||||||
expected = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': '1.6',
|
|
||||||
'cinder_object.data': {'foo': 1}}
|
|
||||||
obj = MyObj(foo=1)
|
|
||||||
obj.obj_reset_changes()
|
|
||||||
self.assertEqual(obj.obj_to_primitive(), expected)
|
|
||||||
|
|
||||||
def test_object_property(self):
|
|
||||||
obj = MyObj(foo=1)
|
|
||||||
self.assertEqual(obj.foo, 1)
|
|
||||||
|
|
||||||
def test_object_property_type_error(self):
|
|
||||||
obj = MyObj()
|
|
||||||
|
|
||||||
def fail():
|
|
||||||
obj.foo = 'a'
|
|
||||||
self.assertRaises(ValueError, fail)
|
|
||||||
|
|
||||||
def test_object_dict_syntax(self):
|
|
||||||
obj = MyObj(foo=123, bar='bar')
|
|
||||||
self.assertEqual(obj['foo'], 123)
|
|
||||||
self.assertEqual(sorted(obj.items(), key=lambda x: x[0]),
|
|
||||||
[('bar', 'bar'), ('foo', 123)])
|
|
||||||
self.assertEqual(sorted(list(obj.iteritems()), key=lambda x: x[0]),
|
|
||||||
[('bar', 'bar'), ('foo', 123)])
|
|
||||||
|
|
||||||
def test_load(self):
|
|
||||||
obj = MyObj()
|
|
||||||
self.assertEqual(obj.bar, 'loaded!')
|
|
||||||
|
|
||||||
def test_load_in_base(self):
|
|
||||||
class Foo(base.CinderObject):
|
|
||||||
fields = {'foobar': fields.Field(fields.Integer())}
|
|
||||||
obj = Foo()
|
|
||||||
with self.assertRaisesRegex(NotImplementedError, ".*foobar.*"):
|
|
||||||
obj.foobar
|
|
||||||
|
|
||||||
def test_loaded_in_primitive(self):
|
|
||||||
obj = MyObj(foo=1)
|
|
||||||
obj.obj_reset_changes()
|
|
||||||
self.assertEqual(obj.bar, 'loaded!')
|
|
||||||
expected = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': '1.6',
|
|
||||||
'cinder_object.changes': ['bar'],
|
|
||||||
'cinder_object.data': {'foo': 1,
|
|
||||||
'bar': 'loaded!'}}
|
|
||||||
self.assertEqual(obj.obj_to_primitive(), expected)
|
|
||||||
|
|
||||||
def test_changes_in_primitive(self):
|
|
||||||
obj = MyObj(foo=123)
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
|
||||||
primitive = obj.obj_to_primitive()
|
|
||||||
self.assertIn('cinder_object.changes', primitive)
|
|
||||||
obj2 = MyObj.obj_from_primitive(primitive)
|
|
||||||
self.assertEqual(obj2.obj_what_changed(), set(['foo']))
|
|
||||||
obj2.obj_reset_changes()
|
|
||||||
self.assertEqual(obj2.obj_what_changed(), set())
|
|
||||||
|
|
||||||
def test_obj_class_from_name(self):
|
|
||||||
obj = base.CinderObject.obj_class_from_name('MyObj', '1.5')
|
|
||||||
self.assertEqual('1.5', obj.VERSION)
|
|
||||||
|
|
||||||
def test_obj_class_from_name_latest_compatible(self):
|
|
||||||
obj = base.CinderObject.obj_class_from_name('MyObj', '1.1')
|
|
||||||
self.assertEqual('1.6', obj.VERSION)
|
|
||||||
|
|
||||||
def test_unknown_objtype(self):
|
|
||||||
self.assertRaises(exception.UnsupportedObjectError,
|
|
||||||
base.CinderObject.obj_class_from_name, 'foo', '1.0')
|
|
||||||
|
|
||||||
def test_obj_class_from_name_supported_version(self):
|
|
||||||
error = None
|
|
||||||
try:
|
|
||||||
base.CinderObject.obj_class_from_name('MyObj', '1.25')
|
|
||||||
except exception.IncompatibleObjectVersion as error:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.assertIsNotNone(error)
|
|
||||||
self.assertEqual('1.6', error.kwargs['supported'])
|
|
||||||
|
|
||||||
def test_with_alternate_context(self):
|
|
||||||
ctxt1 = context.RequestContext('foo', 'foo')
|
|
||||||
ctxt2 = context.RequestContext('bar', 'alternate')
|
|
||||||
obj = MyObj.query(ctxt1)
|
|
||||||
obj._update_test(ctxt2)
|
|
||||||
self.assertEqual(obj.bar, 'alternate-context')
|
|
||||||
self.assertRemotes()
|
|
||||||
|
|
||||||
def test_orphaned_object(self):
|
|
||||||
obj = MyObj.query(self.context)
|
|
||||||
obj._context = None
|
|
||||||
self.assertRaises(exception.OrphanedObjectError,
|
|
||||||
obj._update_test)
|
|
||||||
self.assertRemotes()
|
|
||||||
|
|
||||||
def test_changed_1(self):
|
|
||||||
obj = MyObj.query(self.context)
|
|
||||||
obj.foo = 123
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
|
||||||
obj._update_test(self.context)
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set(['foo', 'bar']))
|
|
||||||
self.assertEqual(obj.foo, 123)
|
|
||||||
self.assertRemotes()
|
|
||||||
|
|
||||||
def test_changed_2(self):
|
|
||||||
obj = MyObj.query(self.context)
|
|
||||||
obj.foo = 123
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
|
||||||
obj.save()
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set([]))
|
|
||||||
self.assertEqual(obj.foo, 123)
|
|
||||||
self.assertRemotes()
|
|
||||||
|
|
||||||
def test_changed_3(self):
|
|
||||||
obj = MyObj.query(self.context)
|
|
||||||
obj.foo = 123
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
|
||||||
obj.refresh()
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set([]))
|
|
||||||
self.assertEqual(obj.foo, 321)
|
|
||||||
self.assertEqual(obj.bar, 'refreshed')
|
|
||||||
self.assertRemotes()
|
|
||||||
|
|
||||||
def test_changed_4(self):
|
|
||||||
obj = MyObj.query(self.context)
|
|
||||||
obj.bar = 'something'
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set(['bar']))
|
|
||||||
obj.modify_save_modify(self.context)
|
|
||||||
self.assertEqual(obj.obj_what_changed(), set(['foo', 'rel_object']))
|
|
||||||
self.assertEqual(obj.foo, 42)
|
|
||||||
self.assertEqual(obj.bar, 'meow')
|
|
||||||
self.assertIsInstance(obj.rel_object, MyOwnedObject)
|
|
||||||
self.assertRemotes()
|
|
||||||
|
|
||||||
def test_changed_with_sub_object(self):
|
|
||||||
class ParentObject(base.CinderObject):
|
|
||||||
fields = {'foo': fields.IntegerField(),
|
|
||||||
'bar': fields.ObjectField('MyObj'),
|
|
||||||
}
|
|
||||||
obj = ParentObject()
|
|
||||||
self.assertEqual(set(), obj.obj_what_changed())
|
|
||||||
obj.foo = 1
|
|
||||||
self.assertEqual(set(['foo']), obj.obj_what_changed())
|
|
||||||
bar = MyObj()
|
|
||||||
obj.bar = bar
|
|
||||||
self.assertEqual(set(['foo', 'bar']), obj.obj_what_changed())
|
|
||||||
obj.obj_reset_changes()
|
|
||||||
self.assertEqual(set(), obj.obj_what_changed())
|
|
||||||
bar.foo = 1
|
|
||||||
self.assertEqual(set(['bar']), obj.obj_what_changed())
|
|
||||||
|
|
||||||
def test_static_result(self):
|
|
||||||
obj = MyObj.query(self.context)
|
|
||||||
self.assertEqual(obj.bar, 'bar')
|
|
||||||
result = obj.marco()
|
|
||||||
self.assertEqual(result, 'polo')
|
|
||||||
self.assertRemotes()
|
|
||||||
|
|
||||||
def test_updates(self):
|
|
||||||
obj = MyObj.query(self.context)
|
|
||||||
self.assertEqual(obj.foo, 1)
|
|
||||||
obj._update_test()
|
|
||||||
self.assertEqual(obj.bar, 'updated')
|
|
||||||
self.assertRemotes()
|
|
||||||
|
|
||||||
def test_base_attributes(self):
|
|
||||||
dt = datetime.datetime(1955, 11, 5)
|
|
||||||
obj = MyObj(created_at=dt, updated_at=dt, deleted_at=None,
|
|
||||||
deleted=False)
|
|
||||||
expected = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': '1.6',
|
|
||||||
'cinder_object.changes':
|
|
||||||
['created_at', 'deleted', 'deleted_at', 'updated_at'],
|
|
||||||
'cinder_object.data':
|
|
||||||
{'created_at': timeutils.isotime(dt),
|
|
||||||
'updated_at': timeutils.isotime(dt),
|
|
||||||
'deleted_at': None,
|
|
||||||
'deleted': False,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.assertEqual(obj.obj_to_primitive(), expected)
|
|
||||||
|
|
||||||
def test_contains(self):
|
|
||||||
obj = MyObj()
|
|
||||||
self.assertNotIn('foo', obj)
|
|
||||||
obj.foo = 1
|
|
||||||
self.assertIn('foo', obj)
|
|
||||||
self.assertNotIn('does_not_exist', obj)
|
|
||||||
|
|
||||||
def test_obj_attr_is_set(self):
|
|
||||||
obj = MyObj(foo=1)
|
|
||||||
self.assertTrue(obj.obj_attr_is_set('foo'))
|
|
||||||
self.assertFalse(obj.obj_attr_is_set('bar'))
|
|
||||||
self.assertRaises(AttributeError, obj.obj_attr_is_set, 'bang')
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
obj = MyObj(foo=1)
|
|
||||||
# Foo has value, should not get the default
|
|
||||||
self.assertEqual(1, obj.get('foo', 2))
|
|
||||||
# Foo has value, should return the value without error
|
|
||||||
self.assertEqual(1, obj.get('foo'))
|
|
||||||
# Bar is not loaded, so we should get the default
|
|
||||||
self.assertEqual('not-loaded', obj.get('bar', 'not-loaded'))
|
|
||||||
# Bar without a default should lazy-load
|
|
||||||
self.assertEqual('loaded!', obj.get('bar'))
|
|
||||||
# Bar now has a default, but loaded value should be returned
|
|
||||||
self.assertEqual('loaded!', obj.get('bar', 'not-loaded'))
|
|
||||||
# Invalid attribute should return None
|
|
||||||
self.assertEqual(None, obj.get('nothing'))
|
|
||||||
|
|
||||||
def test_object_inheritance(self):
|
|
||||||
base_fields = base.CinderPersistentObject.fields.keys()
|
|
||||||
myobj_fields = (['foo', 'bar', 'missing',
|
|
||||||
'readonly', 'rel_object', 'rel_objects'] +
|
|
||||||
base_fields)
|
|
||||||
myobj3_fields = ['new_field']
|
|
||||||
self.assertTrue(issubclass(TestSubclassedObject, MyObj))
|
|
||||||
self.assertEqual(len(myobj_fields), len(MyObj.fields))
|
|
||||||
self.assertEqual(set(myobj_fields), set(MyObj.fields.keys()))
|
|
||||||
self.assertEqual(len(myobj_fields) + len(myobj3_fields),
|
|
||||||
len(TestSubclassedObject.fields))
|
|
||||||
self.assertEqual(set(myobj_fields) | set(myobj3_fields),
|
|
||||||
set(TestSubclassedObject.fields.keys()))
|
|
||||||
|
|
||||||
def test_obj_as_admin(self):
|
|
||||||
obj = MyObj(context=self.context)
|
|
||||||
|
|
||||||
def fake(*args, **kwargs):
|
|
||||||
self.assertTrue(obj._context.is_admin)
|
|
||||||
|
|
||||||
with mock.patch.object(obj, 'obj_reset_changes') as mock_fn:
|
|
||||||
mock_fn.side_effect = fake
|
|
||||||
with obj.obj_as_admin():
|
|
||||||
obj.save()
|
|
||||||
self.assertTrue(mock_fn.called)
|
|
||||||
|
|
||||||
self.assertFalse(obj._context.is_admin)
|
|
||||||
|
|
||||||
def test_obj_as_admin_orphaned(self):
|
|
||||||
def testme():
|
|
||||||
obj = MyObj()
|
|
||||||
with obj.obj_as_admin():
|
|
||||||
pass
|
|
||||||
self.assertRaises(exception.OrphanedObjectError, testme)
|
|
||||||
|
|
||||||
def test_get_changes(self):
|
|
||||||
obj = MyObj()
|
|
||||||
self.assertEqual({}, obj.obj_get_changes())
|
|
||||||
obj.foo = 123
|
|
||||||
self.assertEqual({'foo': 123}, obj.obj_get_changes())
|
|
||||||
obj.bar = 'test'
|
|
||||||
self.assertEqual({'foo': 123, 'bar': 'test'}, obj.obj_get_changes())
|
|
||||||
obj.obj_reset_changes()
|
|
||||||
self.assertEqual({}, obj.obj_get_changes())
|
|
||||||
|
|
||||||
def test_obj_fields(self):
|
|
||||||
class TestObj(base.CinderObject):
|
|
||||||
fields = {'foo': fields.Field(fields.Integer())}
|
|
||||||
obj_extra_fields = ['bar']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bar(self):
|
|
||||||
return 'this is bar'
|
|
||||||
|
|
||||||
obj = TestObj()
|
|
||||||
self.assertEqual(['foo', 'bar'], obj.obj_fields)
|
|
||||||
|
|
||||||
def test_obj_constructor(self):
|
|
||||||
obj = MyObj(context=self.context, foo=123, bar='abc')
|
|
||||||
self.assertEqual(123, obj.foo)
|
|
||||||
self.assertEqual('abc', obj.bar)
|
|
||||||
self.assertEqual(set(['foo', 'bar']), obj.obj_what_changed())
|
|
||||||
|
|
||||||
def test_obj_read_only(self):
|
|
||||||
obj = MyObj(context=self.context, foo=123, bar='abc')
|
|
||||||
obj.readonly = 1
|
|
||||||
self.assertRaises(exception.ReadOnlyFieldError, setattr,
|
|
||||||
obj, 'readonly', 2)
|
|
||||||
|
|
||||||
def test_obj_repr(self):
|
|
||||||
obj = MyObj(foo=123)
|
|
||||||
self.assertEqual('MyObj(bar=<?>,created_at=<?>,deleted=<?>,'
|
|
||||||
'deleted_at=<?>,foo=123,missing=<?>,readonly=<?>,'
|
|
||||||
'rel_object=<?>,rel_objects=<?>,updated_at=<?>)',
|
|
||||||
repr(obj))
|
|
||||||
|
|
||||||
def test_obj_make_obj_compatible(self):
|
|
||||||
subobj = MyOwnedObject(baz=1)
|
|
||||||
obj = MyObj(rel_object=subobj)
|
|
||||||
obj.obj_relationships = {
|
|
||||||
'rel_object': [('1.5', '1.1'), ('1.7', '1.2')],
|
|
||||||
}
|
|
||||||
primitive = obj.obj_to_primitive()['cinder_object.data']
|
|
||||||
with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
|
|
||||||
obj._obj_make_obj_compatible(copy.copy(primitive), '1.8',
|
|
||||||
'rel_object')
|
|
||||||
self.assertFalse(mock_compat.called)
|
|
||||||
|
|
||||||
with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
|
|
||||||
obj._obj_make_obj_compatible(copy.copy(primitive),
|
|
||||||
'1.7', 'rel_object')
|
|
||||||
mock_compat.assert_called_once_with(
|
|
||||||
primitive['rel_object']['cinder_object.data'], '1.2')
|
|
||||||
self.assertEqual('1.2',
|
|
||||||
primitive['rel_object']['cinder_object.version'])
|
|
||||||
|
|
||||||
with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
|
|
||||||
obj._obj_make_obj_compatible(copy.copy(primitive),
|
|
||||||
'1.6', 'rel_object')
|
|
||||||
mock_compat.assert_called_once_with(
|
|
||||||
primitive['rel_object']['cinder_object.data'], '1.1')
|
|
||||||
self.assertEqual('1.1',
|
|
||||||
primitive['rel_object']['cinder_object.version'])
|
|
||||||
|
|
||||||
with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
|
|
||||||
obj._obj_make_obj_compatible(copy.copy(primitive), '1.5',
|
|
||||||
'rel_object')
|
|
||||||
mock_compat.assert_called_once_with(
|
|
||||||
primitive['rel_object']['cinder_object.data'], '1.1')
|
|
||||||
self.assertEqual('1.1',
|
|
||||||
primitive['rel_object']['cinder_object.version'])
|
|
||||||
|
|
||||||
with mock.patch.object(subobj, 'obj_make_compatible') as mock_compat:
|
|
||||||
_prim = copy.copy(primitive)
|
|
||||||
obj._obj_make_obj_compatible(_prim, '1.4', 'rel_object')
|
|
||||||
self.assertFalse(mock_compat.called)
|
|
||||||
self.assertNotIn('rel_object', _prim)
|
|
||||||
|
|
||||||
def test_obj_make_compatible_hits_sub_objects(self):
|
|
||||||
subobj = MyOwnedObject(baz=1)
|
|
||||||
obj = MyObj(foo=123, rel_object=subobj)
|
|
||||||
obj.obj_relationships = {'rel_object': [('1.0', '1.0')]}
|
|
||||||
with mock.patch.object(obj, '_obj_make_obj_compatible') as mock_compat:
|
|
||||||
obj.obj_make_compatible({'rel_object': 'foo'}, '1.10')
|
|
||||||
mock_compat.assert_called_once_with({'rel_object': 'foo'}, '1.10',
|
|
||||||
'rel_object')
|
|
||||||
|
|
||||||
def test_obj_make_compatible_skips_unset_sub_objects(self):
|
|
||||||
obj = MyObj(foo=123)
|
|
||||||
obj.obj_relationships = {'rel_object': [('1.0', '1.0')]}
|
|
||||||
with mock.patch.object(obj, '_obj_make_obj_compatible') as mock_compat:
|
|
||||||
obj.obj_make_compatible({'rel_object': 'foo'}, '1.10')
|
|
||||||
self.assertFalse(mock_compat.called)
|
|
||||||
|
|
||||||
def test_obj_make_compatible_complains_about_missing_rules(self):
|
|
||||||
subobj = MyOwnedObject(baz=1)
|
|
||||||
obj = MyObj(foo=123, rel_object=subobj)
|
|
||||||
obj.obj_relationships = {}
|
|
||||||
self.assertRaises(exception.ObjectActionError,
|
|
||||||
obj.obj_make_compatible, {}, '1.0')
|
|
||||||
|
|
||||||
def test_obj_make_compatible_handles_list_of_objects(self):
|
|
||||||
subobj = MyOwnedObject(baz=1)
|
|
||||||
obj = MyObj(rel_objects=[subobj])
|
|
||||||
obj.obj_relationships = {'rel_objects': [('1.0', '1.123')]}
|
|
||||||
|
|
||||||
def fake_make_compat(primitive, version):
|
|
||||||
self.assertEqual('1.123', version)
|
|
||||||
self.assertIn('baz', primitive)
|
|
||||||
|
|
||||||
with mock.patch.object(subobj, 'obj_make_compatible') as mock_mc:
|
|
||||||
mock_mc.side_effect = fake_make_compat
|
|
||||||
obj.obj_to_primitive('1.0')
|
|
||||||
self.assertTrue(mock_mc.called)
|
|
||||||
|
|
||||||
|
|
||||||
class TestObject(_LocalTest, _TestObject):
|
|
||||||
def test_set_defaults(self):
|
|
||||||
obj = MyObj()
|
|
||||||
obj.obj_set_defaults('foo')
|
|
||||||
self.assertTrue(obj.obj_attr_is_set('foo'))
|
|
||||||
self.assertEqual(1, obj.foo)
|
|
||||||
|
|
||||||
def test_set_defaults_no_default(self):
|
|
||||||
obj = MyObj()
|
|
||||||
self.assertRaises(exception.ObjectActionError,
|
|
||||||
obj.obj_set_defaults, 'bar')
|
|
||||||
|
|
||||||
def test_set_all_defaults(self):
|
|
||||||
obj = MyObj()
|
|
||||||
obj.obj_set_defaults()
|
|
||||||
self.assertEqual(set(['deleted', 'foo']), obj.obj_what_changed())
|
|
||||||
self.assertEqual(1, obj.foo)
|
|
||||||
|
|
||||||
|
|
||||||
class TestObjectListBase(test.TestCase):
|
|
||||||
def test_list_like_operations(self):
|
|
||||||
class MyElement(base.CinderObject):
|
|
||||||
fields = {'foo': fields.IntegerField()}
|
|
||||||
|
|
||||||
def __init__(self, foo):
|
|
||||||
super(MyElement, self).__init__()
|
|
||||||
self.foo = foo
|
|
||||||
|
|
||||||
class Foo(base.ObjectListBase, base.CinderObject):
|
|
||||||
fields = {'objects': fields.ListOfObjectsField('MyElement')}
|
|
||||||
|
|
||||||
objlist = Foo(context='foo',
|
|
||||||
objects=[MyElement(1), MyElement(2), MyElement(3)])
|
|
||||||
self.assertEqual(list(objlist), objlist.objects)
|
|
||||||
self.assertEqual(len(objlist), 3)
|
|
||||||
self.assertIn(objlist.objects[0], objlist)
|
|
||||||
self.assertEqual(list(objlist[:1]), [objlist.objects[0]])
|
|
||||||
self.assertEqual(objlist[:1]._context, 'foo')
|
|
||||||
self.assertEqual(objlist[2], objlist.objects[2])
|
|
||||||
self.assertEqual(objlist.count(objlist.objects[0]), 1)
|
|
||||||
self.assertEqual(objlist.index(objlist.objects[1]), 1)
|
|
||||||
objlist.sort(key=lambda x: x.foo, reverse=True)
|
|
||||||
self.assertEqual([3, 2, 1],
|
|
||||||
[x.foo for x in objlist])
|
|
||||||
|
|
||||||
def test_serialization(self):
|
|
||||||
class Foo(base.ObjectListBase, base.CinderObject):
|
|
||||||
fields = {'objects': fields.ListOfObjectsField('Bar')}
|
|
||||||
|
|
||||||
class Bar(base.CinderObject):
|
|
||||||
fields = {'foo': fields.Field(fields.String())}
|
|
||||||
|
|
||||||
obj = Foo(objects=[])
|
|
||||||
for i in 'abc':
|
|
||||||
bar = Bar(foo=i)
|
|
||||||
obj.objects.append(bar)
|
|
||||||
|
|
||||||
obj2 = base.CinderObject.obj_from_primitive(obj.obj_to_primitive())
|
|
||||||
self.assertFalse(obj is obj2)
|
|
||||||
self.assertEqual([x.foo for x in obj],
|
|
||||||
[y.foo for y in obj2])
|
|
||||||
|
|
||||||
def test_list_changes(self):
|
|
||||||
class Foo(base.ObjectListBase, base.CinderObject):
|
|
||||||
fields = {'objects': fields.ListOfObjectsField('Bar')}
|
|
||||||
|
|
||||||
class Bar(base.CinderObject):
|
|
||||||
fields = {'foo': fields.StringField()}
|
|
||||||
|
|
||||||
obj = Foo(objects=[])
|
|
||||||
self.assertEqual(set(['objects']), obj.obj_what_changed())
|
|
||||||
obj.objects.append(Bar(foo='test'))
|
|
||||||
self.assertEqual(set(['objects']), obj.obj_what_changed())
|
|
||||||
obj.obj_reset_changes()
|
|
||||||
# This should still look dirty because the child is dirty
|
|
||||||
self.assertEqual(set(['objects']), obj.obj_what_changed())
|
|
||||||
obj.objects[0].obj_reset_changes()
|
|
||||||
# This should now look clean because the child is clean
|
|
||||||
self.assertEqual(set(), obj.obj_what_changed())
|
|
||||||
|
|
||||||
def test_initialize_objects(self):
|
|
||||||
class Foo(base.ObjectListBase, base.CinderObject):
|
|
||||||
fields = {'objects': fields.ListOfObjectsField('Bar')}
|
|
||||||
|
|
||||||
class Bar(base.CinderObject):
|
|
||||||
fields = {'foo': fields.StringField()}
|
|
||||||
|
|
||||||
obj = Foo()
|
|
||||||
self.assertEqual([], obj.objects)
|
|
||||||
self.assertEqual(set(), obj.obj_what_changed())
|
|
||||||
|
|
||||||
def test_obj_repr(self):
|
|
||||||
class Foo(base.ObjectListBase, base.CinderObject):
|
|
||||||
fields = {'objects': fields.ListOfObjectsField('Bar')}
|
|
||||||
|
|
||||||
class Bar(base.CinderObject):
|
|
||||||
fields = {'uuid': fields.StringField()}
|
|
||||||
|
|
||||||
obj = Foo(objects=[Bar(uuid='fake-uuid')])
|
|
||||||
self.assertEqual('Foo(objects=[Bar(fake-uuid)])', repr(obj))
|
|
||||||
|
|
||||||
|
|
||||||
class TestObjectSerializer(_BaseTestCase):
|
|
||||||
def test_serialize_entity_primitive(self):
|
|
||||||
ser = base.CinderObjectSerializer()
|
|
||||||
for thing in (1, 'foo', [1, 2], {'foo': 'bar'}):
|
|
||||||
self.assertEqual(thing, ser.serialize_entity(None, thing))
|
|
||||||
|
|
||||||
def test_deserialize_entity_primitive(self):
|
|
||||||
ser = base.CinderObjectSerializer()
|
|
||||||
for thing in (1, 'foo', [1, 2], {'foo': 'bar'}):
|
|
||||||
self.assertEqual(thing, ser.deserialize_entity(None, thing))
|
|
||||||
|
|
||||||
def _test_deserialize_entity_newer(self, obj_version, backported_to,
|
|
||||||
my_version='1.6'):
|
|
||||||
ser = base.CinderObjectSerializer()
|
|
||||||
ser._conductor = mock.Mock()
|
|
||||||
ser._conductor.object_backport.return_value = 'backported'
|
|
||||||
|
|
||||||
class MyTestObj(MyObj):
|
|
||||||
VERSION = my_version
|
|
||||||
|
|
||||||
obj = MyTestObj()
|
|
||||||
obj.VERSION = obj_version
|
|
||||||
primitive = obj.obj_to_primitive()
|
|
||||||
ser.deserialize_entity(self.context, primitive)
|
|
||||||
if backported_to is None:
|
|
||||||
self.assertFalse(ser._conductor.object_backport.called)
|
|
||||||
|
|
||||||
def test_deserialize_entity_newer_revision_does_not_backport_zero(self):
|
|
||||||
self._test_deserialize_entity_newer('1.6.0', None)
|
|
||||||
|
|
||||||
def test_deserialize_entity_newer_revision_does_not_backport(self):
|
|
||||||
self._test_deserialize_entity_newer('1.6.1', None)
|
|
||||||
|
|
||||||
def test_deserialize_dot_z_with_extra_stuff(self):
|
|
||||||
primitive = {'cinder_object.name': 'MyObj',
|
|
||||||
'cinder_object.namespace': 'cinder',
|
|
||||||
'cinder_object.version': '1.6.1',
|
|
||||||
'cinder_object.data': {
|
|
||||||
'foo': 1,
|
|
||||||
'unexpected_thing': 'foobar'}}
|
|
||||||
ser = base.CinderObjectSerializer()
|
|
||||||
obj = ser.deserialize_entity(self.context, primitive)
|
|
||||||
self.assertEqual(1, obj.foo)
|
|
||||||
self.assertFalse(hasattr(obj, 'unexpected_thing'))
|
|
||||||
# NOTE(danms): The serializer is where the logic lives that
|
|
||||||
# avoids backports for cases where only a .z difference in
|
|
||||||
# the received object version is detected. As a result, we
|
|
||||||
# end up with a version of what we expected, effectively the
|
|
||||||
# .0 of the object.
|
|
||||||
self.assertEqual('1.6', obj.VERSION)
|
|
||||||
|
|
||||||
def test_object_serialization(self):
|
|
||||||
ser = base.CinderObjectSerializer()
|
|
||||||
obj = MyObj()
|
|
||||||
primitive = ser.serialize_entity(self.context, obj)
|
|
||||||
self.assertIn('cinder_object.name', primitive)
|
|
||||||
obj2 = ser.deserialize_entity(self.context, primitive)
|
|
||||||
self.assertIsInstance(obj2, MyObj)
|
|
||||||
self.assertEqual(self.context, obj2._context)
|
|
||||||
|
|
||||||
def test_object_serialization_iterables(self):
|
|
||||||
ser = base.CinderObjectSerializer()
|
|
||||||
obj = MyObj()
|
|
||||||
for iterable in (list, tuple, set):
|
|
||||||
thing = iterable([obj])
|
|
||||||
primitive = ser.serialize_entity(self.context, thing)
|
|
||||||
self.assertEqual(1, len(primitive))
|
|
||||||
for item in primitive:
|
|
||||||
self.assertNotIsInstance(item, base.CinderObject)
|
|
||||||
thing2 = ser.deserialize_entity(self.context, primitive)
|
|
||||||
self.assertEqual(1, len(thing2))
|
|
||||||
for item in thing2:
|
|
||||||
self.assertIsInstance(item, MyObj)
|
|
||||||
# dict case
|
|
||||||
thing = {'key': obj}
|
|
||||||
primitive = ser.serialize_entity(self.context, thing)
|
|
||||||
self.assertEqual(1, len(primitive))
|
|
||||||
for item in primitive.itervalues():
|
|
||||||
self.assertNotIsInstance(item, base.CinderObject)
|
|
||||||
thing2 = ser.deserialize_entity(self.context, primitive)
|
|
||||||
self.assertEqual(1, len(thing2))
|
|
||||||
for item in thing2.itervalues():
|
|
||||||
self.assertIsInstance(item, MyObj)
|
|
||||||
|
|
||||||
# object-action updates dict case
|
|
||||||
thing = {'foo': obj.obj_to_primitive()}
|
|
||||||
primitive = ser.serialize_entity(self.context, thing)
|
|
||||||
self.assertEqual(thing, primitive)
|
|
||||||
thing2 = ser.deserialize_entity(self.context, thing)
|
|
||||||
self.assertIsInstance(thing2['foo'], base.CinderObject)
|
|
@ -13,11 +13,11 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
from oslo_versionedobjects.tests import test_objects
|
||||||
|
|
||||||
from cinder.objects import snapshot as snapshot_obj
|
from cinder.objects import snapshot as snapshot_obj
|
||||||
from cinder.objects import volume as volume_obj
|
from cinder.objects import volume as volume_obj
|
||||||
from cinder.tests.unit import fake_volume
|
from cinder.tests.unit import fake_volume
|
||||||
from cinder.tests.unit.objects import test_objects
|
|
||||||
|
|
||||||
fake_snapshot = {
|
fake_snapshot = {
|
||||||
'id': '1',
|
'id': '1',
|
||||||
@ -68,7 +68,7 @@ class TestSnapshot(test_objects._LocalTest):
|
|||||||
snapshot = snapshot_obj.Snapshot._from_db_object(
|
snapshot = snapshot_obj.Snapshot._from_db_object(
|
||||||
self.context, snapshot_obj.Snapshot(), fake_snapshot)
|
self.context, snapshot_obj.Snapshot(), fake_snapshot)
|
||||||
snapshot.display_name = 'foobar'
|
snapshot.display_name = 'foobar'
|
||||||
snapshot.save(self.context)
|
snapshot.save()
|
||||||
snapshot_update.assert_called_once_with(self.context, snapshot.id,
|
snapshot_update.assert_called_once_with(self.context, snapshot.id,
|
||||||
{'display_name': 'foobar'})
|
{'display_name': 'foobar'})
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ class TestSnapshot(test_objects._LocalTest):
|
|||||||
self.assertEqual({'display_name': 'foobar',
|
self.assertEqual({'display_name': 'foobar',
|
||||||
'metadata': {'key1': 'value1'}},
|
'metadata': {'key1': 'value1'}},
|
||||||
snapshot.obj_get_changes())
|
snapshot.obj_get_changes())
|
||||||
snapshot.save(self.context)
|
snapshot.save()
|
||||||
snapshot_update.assert_called_once_with(self.context, snapshot.id,
|
snapshot_update.assert_called_once_with(self.context, snapshot.id,
|
||||||
{'display_name': 'foobar'})
|
{'display_name': 'foobar'})
|
||||||
snapshot_metadata_update.assert_called_once_with(self.context, '1',
|
snapshot_metadata_update.assert_called_once_with(self.context, '1',
|
||||||
@ -128,7 +128,7 @@ class TestSnapshot(test_objects._LocalTest):
|
|||||||
|
|
||||||
class TestSnapshotList(test_objects._LocalTest):
|
class TestSnapshotList(test_objects._LocalTest):
|
||||||
@mock.patch('cinder.db.snapshot_metadata_get', return_value={})
|
@mock.patch('cinder.db.snapshot_metadata_get', return_value={})
|
||||||
@mock.patch('cinder.objects.Volume.get_by_id')
|
@mock.patch('cinder.objects.volume.Volume.get_by_id')
|
||||||
@mock.patch('cinder.db.snapshot_get_all', return_value=[fake_snapshot])
|
@mock.patch('cinder.db.snapshot_get_all', return_value=[fake_snapshot])
|
||||||
def test_get_all(self, snapshot_get_all, volume_get_by_id,
|
def test_get_all(self, snapshot_get_all, volume_get_by_id,
|
||||||
snapshot_metadata_get):
|
snapshot_metadata_get):
|
||||||
@ -140,7 +140,7 @@ class TestSnapshotList(test_objects._LocalTest):
|
|||||||
TestSnapshot._compare(self, fake_snapshot, snapshots[0])
|
TestSnapshot._compare(self, fake_snapshot, snapshots[0])
|
||||||
|
|
||||||
@mock.patch('cinder.db.snapshot_metadata_get', return_value={})
|
@mock.patch('cinder.db.snapshot_metadata_get', return_value={})
|
||||||
@mock.patch('cinder.objects.Volume.get_by_id')
|
@mock.patch('cinder.objects.volume.Volume.get_by_id')
|
||||||
@mock.patch('cinder.db.snapshot_get_all_by_project',
|
@mock.patch('cinder.db.snapshot_get_all_by_project',
|
||||||
return_value=[fake_snapshot])
|
return_value=[fake_snapshot])
|
||||||
def test_get_all_by_project(self, get_all_by_project, volume_get_by_id,
|
def test_get_all_by_project(self, get_all_by_project, volume_get_by_id,
|
||||||
@ -149,12 +149,12 @@ class TestSnapshotList(test_objects._LocalTest):
|
|||||||
volume_get_by_id.return_value = fake_volume_obj
|
volume_get_by_id.return_value = fake_volume_obj
|
||||||
|
|
||||||
snapshots = snapshot_obj.SnapshotList.get_all_by_project(
|
snapshots = snapshot_obj.SnapshotList.get_all_by_project(
|
||||||
self.context, self.context.project_id)
|
self.context, self.project_id)
|
||||||
self.assertEqual(1, len(snapshots))
|
self.assertEqual(1, len(snapshots))
|
||||||
TestSnapshot._compare(self, fake_snapshot, snapshots[0])
|
TestSnapshot._compare(self, fake_snapshot, snapshots[0])
|
||||||
|
|
||||||
@mock.patch('cinder.db.snapshot_metadata_get', return_value={})
|
@mock.patch('cinder.db.snapshot_metadata_get', return_value={})
|
||||||
@mock.patch('cinder.objects.Volume.get_by_id')
|
@mock.patch('cinder.objects.volume.Volume.get_by_id')
|
||||||
@mock.patch('cinder.db.snapshot_get_all_for_volume',
|
@mock.patch('cinder.db.snapshot_get_all_for_volume',
|
||||||
return_value=[fake_snapshot])
|
return_value=[fake_snapshot])
|
||||||
def test_get_all_for_volume(self, get_all_for_volume, volume_get_by_id,
|
def test_get_all_for_volume(self, get_all_for_volume, volume_get_by_id,
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
from oslo_versionedobjects.tests import test_objects
|
||||||
|
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder.tests.unit import fake_volume
|
from cinder.tests.unit import fake_volume
|
||||||
from cinder.tests.unit.objects import test_objects
|
|
||||||
|
|
||||||
|
|
||||||
class TestVolume(test_objects._LocalTest):
|
class TestVolume(test_objects._LocalTest):
|
||||||
|
@ -105,7 +105,7 @@ def check_policy(context, action, target_obj=None):
|
|||||||
|
|
||||||
if isinstance(target_obj, objects_base.CinderObject):
|
if isinstance(target_obj, objects_base.CinderObject):
|
||||||
# Turn object into dict so target.update can work
|
# Turn object into dict so target.update can work
|
||||||
target.update(objects_base.obj_to_primitive(target_obj) or {})
|
target.update(target_obj.obj_to_primitive() or {})
|
||||||
else:
|
else:
|
||||||
target.update(target_obj or {})
|
target.update(target_obj or {})
|
||||||
|
|
||||||
@ -920,7 +920,7 @@ class API(base.Base):
|
|||||||
|
|
||||||
snapshot_obj = self.get_snapshot(context, snapshot['id'])
|
snapshot_obj = self.get_snapshot(context, snapshot['id'])
|
||||||
snapshot_obj.status = 'deleting'
|
snapshot_obj.status = 'deleting'
|
||||||
snapshot_obj.save(context)
|
snapshot_obj.save()
|
||||||
|
|
||||||
volume = self.db.volume_get(context, snapshot_obj.volume_id)
|
volume = self.db.volume_get(context, snapshot_obj.volume_id)
|
||||||
self.volume_rpcapi.delete_snapshot(context, snapshot_obj,
|
self.volume_rpcapi.delete_snapshot(context, snapshot_obj,
|
||||||
@ -931,7 +931,7 @@ class API(base.Base):
|
|||||||
@wrap_check_policy
|
@wrap_check_policy
|
||||||
def update_snapshot(self, context, snapshot, fields):
|
def update_snapshot(self, context, snapshot, fields):
|
||||||
snapshot.update(fields)
|
snapshot.update(fields)
|
||||||
snapshot.save(context)
|
snapshot.save()
|
||||||
|
|
||||||
@wrap_check_policy
|
@wrap_check_policy
|
||||||
def get_volume_metadata(self, context, volume):
|
def get_volume_metadata(self, context, volume):
|
||||||
@ -1078,7 +1078,7 @@ class API(base.Base):
|
|||||||
self._check_metadata_properties(_metadata)
|
self._check_metadata_properties(_metadata)
|
||||||
|
|
||||||
snapshot.metadata = _metadata
|
snapshot.metadata = _metadata
|
||||||
snapshot.save(context)
|
snapshot.save()
|
||||||
|
|
||||||
# TODO(jdg): Implement an RPC call for drivers that may use this info
|
# TODO(jdg): Implement an RPC call for drivers that may use this info
|
||||||
|
|
||||||
|
@ -643,12 +643,12 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
model_update = self.driver.create_snapshot(snapshot)
|
model_update = self.driver.create_snapshot(snapshot)
|
||||||
if model_update:
|
if model_update:
|
||||||
snapshot.update(model_update)
|
snapshot.update(model_update)
|
||||||
snapshot.save(context)
|
snapshot.save()
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
snapshot.status = 'error'
|
snapshot.status = 'error'
|
||||||
snapshot.save(context)
|
snapshot.save()
|
||||||
|
|
||||||
vol_ref = self.db.volume_get(context, volume_id)
|
vol_ref = self.db.volume_get(context, volume_id)
|
||||||
if vol_ref.bootable:
|
if vol_ref.bootable:
|
||||||
@ -666,12 +666,12 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
" %(volume_id)s metadata"),
|
" %(volume_id)s metadata"),
|
||||||
{'volume_id': volume_id}, resource=snapshot)
|
{'volume_id': volume_id}, resource=snapshot)
|
||||||
snapshot.status = 'error'
|
snapshot.status = 'error'
|
||||||
snapshot.save(context)
|
snapshot.save()
|
||||||
raise exception.MetadataCopyFailure(reason=six.text_type(ex))
|
raise exception.MetadataCopyFailure(reason=six.text_type(ex))
|
||||||
|
|
||||||
snapshot.status = 'available'
|
snapshot.status = 'available'
|
||||||
snapshot.progress = '100%'
|
snapshot.progress = '100%'
|
||||||
snapshot.save(context)
|
snapshot.save()
|
||||||
|
|
||||||
self._notify_about_snapshot_usage(context, snapshot, "create.end")
|
self._notify_about_snapshot_usage(context, snapshot, "create.end")
|
||||||
LOG.info(_LI("Create snapshot completed successfully"),
|
LOG.info(_LI("Create snapshot completed successfully"),
|
||||||
@ -682,6 +682,7 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
def delete_snapshot(self, context, snapshot):
|
def delete_snapshot(self, context, snapshot):
|
||||||
"""Deletes and unexports snapshot."""
|
"""Deletes and unexports snapshot."""
|
||||||
context = context.elevated()
|
context = context.elevated()
|
||||||
|
snapshot._context = context
|
||||||
project_id = snapshot.project_id
|
project_id = snapshot.project_id
|
||||||
|
|
||||||
self._notify_about_snapshot_usage(
|
self._notify_about_snapshot_usage(
|
||||||
@ -731,7 +732,7 @@ class VolumeManager(manager.SchedulerDependentManager):
|
|||||||
LOG.exception(_LE("Update snapshot usages failed."),
|
LOG.exception(_LE("Update snapshot usages failed."),
|
||||||
resource=snapshot)
|
resource=snapshot)
|
||||||
self.db.volume_glance_metadata_delete_by_snapshot(context, snapshot.id)
|
self.db.volume_glance_metadata_delete_by_snapshot(context, snapshot.id)
|
||||||
snapshot.destroy(context)
|
snapshot.destroy()
|
||||||
self._notify_about_snapshot_usage(context, snapshot, "delete.end")
|
self._notify_about_snapshot_usage(context, snapshot, "delete.end")
|
||||||
|
|
||||||
# Commit the reservations
|
# Commit the reservations
|
||||||
|
@ -22,6 +22,7 @@ oslo.middleware>=1.0.0 # Apache-2.0
|
|||||||
oslo.rootwrap>=1.6.0 # Apache-2.0
|
oslo.rootwrap>=1.6.0 # Apache-2.0
|
||||||
oslo.serialization>=1.4.0 # Apache-2.0
|
oslo.serialization>=1.4.0 # Apache-2.0
|
||||||
oslo.utils>=1.4.0 # Apache-2.0
|
oslo.utils>=1.4.0 # Apache-2.0
|
||||||
|
oslo.versionedobjects>=0.1.1
|
||||||
osprofiler>=0.3.0 # Apache-2.0
|
osprofiler>=0.3.0 # Apache-2.0
|
||||||
paramiko>=1.13.0
|
paramiko>=1.13.0
|
||||||
Paste
|
Paste
|
||||||
|
Loading…
Reference in New Issue
Block a user