Add base object model
This adds the base infrastructure for a unified object model in Nova. It provides a somewhat-magical metaclass which serves to automatically register objects that are capable of being remoted. Magic remoting of methods happens via two decorators, allowing a given nova service to globally declare that such actions should happen over RPC to avoid DB accesses. For more details, see blueprint unified-object-model Change-Id: Iecd54ca22666f730c41d2278f74b86d922624a4e
This commit is contained in:
parent
09bb456886
commit
bd8612c120
@ -14,6 +14,8 @@
|
||||
|
||||
"""Handles database requests from other nova services."""
|
||||
|
||||
import copy
|
||||
|
||||
from nova.api.ec2 import ec2utils
|
||||
from nova.compute import api as compute_api
|
||||
from nova.compute import utils as compute_utils
|
||||
@ -22,6 +24,7 @@ from nova import manager
|
||||
from nova import network
|
||||
from nova.network.security_group import openstack_driver
|
||||
from nova import notifications
|
||||
from nova.objects import base as nova_object
|
||||
from nova.openstack.common.db.sqlalchemy import session as db_session
|
||||
from nova.openstack.common import jsonutils
|
||||
from nova.openstack.common import log as logging
|
||||
@ -61,7 +64,7 @@ class ConductorManager(manager.Manager):
|
||||
namespace. See the ComputeTaskManager class for details.
|
||||
"""
|
||||
|
||||
RPC_API_VERSION = '1.49'
|
||||
RPC_API_VERSION = '1.50'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ConductorManager, self).__init__(service_name='conductor',
|
||||
@ -474,6 +477,31 @@ class ConductorManager(manager.Manager):
|
||||
def compute_unrescue(self, context, instance):
|
||||
self.compute_api.unrescue(context, instance)
|
||||
|
||||
def object_class_action(self, context, objname, objmethod,
|
||||
objver, **kwargs):
|
||||
"""Perform a classmethod action on an object."""
|
||||
objclass = nova_object.NovaObject.obj_class_from_name(objname,
|
||||
objver)
|
||||
return getattr(objclass, objmethod)(context, **kwargs)
|
||||
|
||||
def object_action(self, context, objinst, objmethod, **kwargs):
|
||||
"""Perform an action on an object."""
|
||||
oldobj = copy.copy(objinst)
|
||||
result = getattr(objinst, objmethod)(context, **kwargs)
|
||||
updates = dict()
|
||||
# NOTE(danms): Diff the object with the one passed to us and
|
||||
# generate a list of changes to forward back
|
||||
for field in objinst.fields:
|
||||
if not hasattr(objinst, nova_object.get_attrname(field)):
|
||||
# Avoid demand-loading anything
|
||||
continue
|
||||
if oldobj[field] != objinst[field]:
|
||||
updates[field] = objinst._attr_to_primitive(field)
|
||||
# This is safe since a field named this would conflict with the
|
||||
# method anyway
|
||||
updates['obj_what_changed'] = objinst.obj_what_changed()
|
||||
return updates, result
|
||||
|
||||
|
||||
class ComputeTaskManager(object):
|
||||
"""Namespace for compute methods.
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from nova.objects import base as objects_base
|
||||
from nova.openstack.common import jsonutils
|
||||
import nova.openstack.common.rpc.proxy
|
||||
|
||||
@ -88,6 +89,7 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy):
|
||||
instance_get_all_by_filters
|
||||
1.48 - Added compute_unrescue
|
||||
1.49 - Added columns_to_join to instance_get_by_uuid
|
||||
1.50 - Added object_action() and object_class_action()
|
||||
"""
|
||||
|
||||
BASE_RPC_API_VERSION = '1.0'
|
||||
@ -95,7 +97,8 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy):
|
||||
def __init__(self):
|
||||
super(ConductorAPI, self).__init__(
|
||||
topic=CONF.conductor.topic,
|
||||
default_version=self.BASE_RPC_API_VERSION)
|
||||
default_version=self.BASE_RPC_API_VERSION,
|
||||
serializer=objects_base.NovaObjectSerializer())
|
||||
|
||||
def instance_update(self, context, instance_uuid, updates,
|
||||
service=None):
|
||||
@ -441,6 +444,16 @@ class ConductorAPI(nova.openstack.common.rpc.proxy.RpcProxy):
|
||||
msg = self.make_msg('compute_unrescue', instance=instance_p)
|
||||
return self.call(context, msg, version='1.48')
|
||||
|
||||
def object_class_action(self, context, objname, objmethod, objver, kwargs):
|
||||
msg = self.make_msg('object_class_action', objname=objname,
|
||||
objmethod=objmethod, objver=objver, **kwargs)
|
||||
return self.call(context, msg, version='1.50')
|
||||
|
||||
def object_action(self, context, objinst, objmethod, kwargs):
|
||||
msg = self.make_msg('object_action', objinst=objinst,
|
||||
objmethod=objmethod, **kwargs)
|
||||
return self.call(context, msg, version='1.50')
|
||||
|
||||
|
||||
class ComputeTaskAPI(nova.openstack.common.rpc.proxy.RpcProxy):
|
||||
"""Client side of the conductor 'compute' namespaced RPC API
|
||||
|
@ -1208,3 +1208,15 @@ class InstanceFaultRollback(NovaException):
|
||||
message = _("Instance rollback performed due to: %s")
|
||||
self.inner_exception = inner_exception
|
||||
super(InstanceFaultRollback, self).__init__(message % inner_exception)
|
||||
|
||||
|
||||
class UnsupportedObjectError(NovaException):
|
||||
message = _('Unsupported object type %(objtype)s')
|
||||
|
||||
|
||||
class OrphanedObjectError(NovaException):
|
||||
message = _('Cannot call %(method)s on orphaned %(objtype)s object')
|
||||
|
||||
|
||||
class IncompatibleObjectVersion(NovaException):
|
||||
message = _('Version %(objver)s of %(objname)s is not supported')
|
||||
|
@ -57,6 +57,7 @@ from oslo.config import cfg
|
||||
|
||||
from nova import baserpc
|
||||
from nova.db import base
|
||||
from nova.objects import base as objects_base
|
||||
from nova.openstack.common import log as logging
|
||||
from nova.openstack.common import periodic_task
|
||||
from nova.openstack.common.plugin import pluginmanager
|
||||
@ -97,7 +98,8 @@ class Manager(base.Base, periodic_task.PeriodicTasks):
|
||||
apis.extend(additional_apis)
|
||||
base_rpc = baserpc.BaseRPCAPI(self.service_name, backdoor_port)
|
||||
apis.extend([self, base_rpc])
|
||||
return rpc_dispatcher.RpcDispatcher(apis)
|
||||
serializer = objects_base.NovaObjectSerializer()
|
||||
return rpc_dispatcher.RpcDispatcher(apis, serializer)
|
||||
|
||||
def periodic_tasks(self, context, raise_on_error=False):
|
||||
"""Tasks to be run at a periodic interval."""
|
||||
|
13
nova/objects/__init__.py
Normal file
13
nova/objects/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright 2013 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.
|
368
nova/objects/base.py
Normal file
368
nova/objects/base.py
Normal file
@ -0,0 +1,368 @@
|
||||
# Copyright 2013 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.
|
||||
|
||||
"""Nova common internal object model"""
|
||||
|
||||
import collections
|
||||
|
||||
from nova import exception
|
||||
from nova.objects import utils as obj_utils
|
||||
from nova.openstack.common import log as logging
|
||||
import nova.openstack.common.rpc.dispatcher
|
||||
import nova.openstack.common.rpc.proxy
|
||||
import nova.openstack.common.rpc.serializer
|
||||
|
||||
|
||||
LOG = logging.getLogger('object')
|
||||
|
||||
|
||||
def get_attrname(name):
|
||||
"""Return the mangled name of the attribute's underlying storage."""
|
||||
return '_%s' % name
|
||||
|
||||
|
||||
def make_class_properties(cls):
|
||||
# NOTE(danms): Inherit NovaObject's base fields only
|
||||
cls.fields.update(NovaObject.fields)
|
||||
for name, typefn in cls.fields.iteritems():
|
||||
|
||||
def getter(self, name=name, typefn=typefn):
|
||||
attrname = get_attrname(name)
|
||||
if not hasattr(self, attrname):
|
||||
self.obj_load(name)
|
||||
return getattr(self, attrname)
|
||||
|
||||
def setter(self, value, name=name, typefn=typefn):
|
||||
self._changed_fields.add(name)
|
||||
try:
|
||||
return setattr(self, get_attrname(name), typefn(value))
|
||||
except Exception:
|
||||
attr = "%s.%s" % (self.obj_name(), name)
|
||||
LOG.exception(_('Error setting %(attr)s') %
|
||||
{'attr': attr})
|
||||
raise
|
||||
|
||||
setattr(cls, name, property(getter, setter))
|
||||
|
||||
|
||||
class NovaObjectMetaclass(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 will be set in the 'NovaObject' class.
|
||||
cls._obj_classes = collections.defaultdict(list)
|
||||
else:
|
||||
# Add the subclass to NovaObject._obj_classes
|
||||
make_class_properties(cls)
|
||||
cls._obj_classes[cls.obj_name()].append(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."""
|
||||
def wrapper(cls, context, **kwargs):
|
||||
if NovaObject.indirection_api:
|
||||
result = NovaObject.indirection_api.object_class_action(
|
||||
context, cls.obj_name(), fn.__name__, cls.version, kwargs)
|
||||
else:
|
||||
result = fn(cls, context, **kwargs)
|
||||
if isinstance(result, NovaObject):
|
||||
result._context = context
|
||||
return result
|
||||
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."""
|
||||
def wrapper(self, context=None, **kwargs):
|
||||
if context is None:
|
||||
context = self._context
|
||||
if context is None:
|
||||
raise exception.OrphanedObjectError(method=fn.__name__,
|
||||
objtype=self.obj_name())
|
||||
if NovaObject.indirection_api:
|
||||
updates, result = NovaObject.indirection_api.object_action(
|
||||
context, self, fn.__name__, kwargs)
|
||||
for key, value in updates.iteritems():
|
||||
if key in self.fields:
|
||||
self[key] = self._attr_from_primitive(key, value)
|
||||
self._changed_fields = set(updates.get('obj_what_changed', []))
|
||||
return result
|
||||
else:
|
||||
return fn(self, context, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# 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.
|
||||
def check_object_version(server, client):
|
||||
try:
|
||||
client_major, _client_minor = client.split('.')
|
||||
server_major, _server_minor = server.split('.')
|
||||
client_minor = int(_client_minor)
|
||||
server_minor = int(_server_minor)
|
||||
except ValueError:
|
||||
raise exception.IncompatibleObjectVersion(
|
||||
_('Invalid version string'))
|
||||
|
||||
if client_major != server_major:
|
||||
raise exception.IncompatibleObjectVersion(
|
||||
dict(client=client_major, server=server_major))
|
||||
if client_minor > server_minor:
|
||||
raise exception.IncompatibleObjectVersion(
|
||||
dict(client=client_minor, server=server_minor))
|
||||
|
||||
|
||||
class NovaObject(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.
|
||||
"""
|
||||
__metaclass__ = NovaObjectMetaclass
|
||||
|
||||
# Version of this object (see rules above check_object_version())
|
||||
version = '1.0'
|
||||
|
||||
# The fields present in this object as key:typefn pairs. For example:
|
||||
#
|
||||
# fields = { 'foo': int,
|
||||
# 'bar': str,
|
||||
# 'baz': lambda x: str(x).ljust(8),
|
||||
# }
|
||||
#
|
||||
# NOTE(danms): The base NovaObject class' fields will be inherited
|
||||
# by subclasses, but that is a special case. Objects inheriting from
|
||||
# other objects will not receive this merging of fields contents.
|
||||
fields = {
|
||||
'created_at': obj_utils.datetime_or_none,
|
||||
'updated_at': obj_utils.datetime_or_none,
|
||||
'deleted_at': obj_utils.datetime_or_none,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._changed_fields = set()
|
||||
self._context = None
|
||||
|
||||
@classmethod
|
||||
def obj_name(cls):
|
||||
"""Return a canonical name for this object which 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(_('Unable to instantiate unregistered object type '
|
||||
'%(objtype)s') % dict(objtype=objname))
|
||||
raise exception.UnsupportedObjectError(objtype=objname)
|
||||
|
||||
compatible_match = None
|
||||
for objclass in cls._obj_classes[objname]:
|
||||
if objclass.version == objver:
|
||||
return objclass
|
||||
try:
|
||||
check_object_version(objclass.version, objver)
|
||||
compatible_match = objclass
|
||||
except exception.IncompatibleObjectVersion:
|
||||
pass
|
||||
|
||||
if compatible_match:
|
||||
return compatible_match
|
||||
|
||||
raise exception.IncompatibleObjectVersion(objname=objname,
|
||||
objver=objver)
|
||||
|
||||
_attr_created_at_from_primitive = obj_utils.dt_deserializer
|
||||
_attr_updated_at_from_primitive = obj_utils.dt_deserializer
|
||||
_attr_deleted_at_from_primitive = obj_utils.dt_deserializer
|
||||
|
||||
def _attr_from_primitive(self, attribute, value):
|
||||
"""Attribute deserialization dispatcher.
|
||||
|
||||
This calls self._attr_foo_from_primitive(value) for an attribute
|
||||
foo with value, if it exists, otherwise it assumes the value
|
||||
is suitable for the attribute's setter method.
|
||||
"""
|
||||
handler = '_attr_%s_from_primitive' % attribute
|
||||
if hasattr(self, handler):
|
||||
return getattr(self, handler)(value)
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def obj_from_primitive(cls, primitive):
|
||||
"""Simple base-case hydration.
|
||||
|
||||
This calls self._attr_from_primitive() for each item in fields.
|
||||
"""
|
||||
if primitive['nova_object.namespace'] != 'nova':
|
||||
# NOTE(danms): We don't do anything with this now, but it's
|
||||
# there for "the future"
|
||||
raise exception.UnsupportedObjectError(
|
||||
objtype='%s.%s' % (primitive['nova_object.namespace'],
|
||||
primitive['nova_object.name']))
|
||||
objname = primitive['nova_object.name']
|
||||
objver = primitive['nova_object.version']
|
||||
objdata = primitive['nova_object.data']
|
||||
objclass = cls.obj_class_from_name(objname, objver)
|
||||
self = objclass()
|
||||
for name in self.fields:
|
||||
if name in objdata:
|
||||
setattr(self, name,
|
||||
self._attr_from_primitive(name, objdata[name]))
|
||||
changes = primitive.get('nova_object.changes', [])
|
||||
self._changed_fields = set([x for x in changes if x in self.fields])
|
||||
return self
|
||||
|
||||
_attr_created_at_to_primitive = obj_utils.dt_serializer('created_at')
|
||||
_attr_updated_at_to_primitive = obj_utils.dt_serializer('updated_at')
|
||||
_attr_deleted_at_to_primitive = obj_utils.dt_serializer('deleted_at')
|
||||
|
||||
def _attr_to_primitive(self, attribute):
|
||||
"""Attribute serialization dispatcher.
|
||||
|
||||
This calls self._attr_foo_to_primitive() for an attribute foo,
|
||||
if it exists, otherwise it assumes the attribute itself is
|
||||
primitive-enough to be sent over the RPC wire.
|
||||
"""
|
||||
handler = '_attr_%s_to_primitive' % attribute
|
||||
if hasattr(self, handler):
|
||||
return getattr(self, handler)()
|
||||
else:
|
||||
return getattr(self, attribute)
|
||||
|
||||
def obj_to_primitive(self):
|
||||
"""Simple base-case dehydration.
|
||||
|
||||
This calls self._attr_to_primitive() for each item in fields.
|
||||
"""
|
||||
primitive = dict()
|
||||
for name in self.fields:
|
||||
if hasattr(self, get_attrname(name)):
|
||||
primitive[name] = self._attr_to_primitive(name)
|
||||
obj = {'nova_object.name': self.obj_name(),
|
||||
'nova_object.namespace': 'nova',
|
||||
'nova_object.version': self.version,
|
||||
'nova_object.data': primitive}
|
||||
if self.obj_what_changed():
|
||||
obj['nova_object.changes'] = list(self.obj_what_changed())
|
||||
return obj
|
||||
|
||||
def obj_load_attr(self, attrname):
|
||||
"""Load an additional attribute from the real object.
|
||||
|
||||
This should use self._conductor, and cache any data that might
|
||||
be useful for future load operations.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
_("Cannot load '%(attrname)s' in the base class") % locals())
|
||||
|
||||
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 list of fields that have been modified."""
|
||||
return self._changed_fields
|
||||
|
||||
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()
|
||||
|
||||
# dictish syntactic sugar
|
||||
def iteritems(self):
|
||||
"""For backwards-compatibility with dict-based objects.
|
||||
|
||||
NOTE(danms): May be removed in the future.
|
||||
"""
|
||||
for name in self.fields:
|
||||
if hasattr(self, get_attrname(name)):
|
||||
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 get(self, key, value=None):
|
||||
"""For backwards-compatibility with dict-based objects.
|
||||
|
||||
NOTE(danms): May be removed in the future.
|
||||
"""
|
||||
return self[key]
|
||||
|
||||
|
||||
class NovaObjectSerializer(nova.openstack.common.rpc.serializer.Serializer):
|
||||
"""A NovaObject-aware Serializer.
|
||||
|
||||
This implements the Oslo Serializer interface and provides the
|
||||
ability to serialize and deserialize NovaObject entities. Any service
|
||||
that needs to accept or return NovaObjects as arguments or result values
|
||||
should pass this to its RpcProxy and RpcDispatcher objects.
|
||||
"""
|
||||
def serialize_entity(self, context, entity):
|
||||
if (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 'nova_object.name' in entity:
|
||||
entity = NovaObject.obj_from_primitive(entity)
|
||||
entity._context = context
|
||||
return entity
|
71
nova/objects/utils.py
Normal file
71
nova/objects/utils.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Copyright 2013 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.
|
||||
|
||||
"""Utility methods for objects"""
|
||||
|
||||
import datetime
|
||||
import netaddr
|
||||
|
||||
from nova.openstack.common import timeutils
|
||||
|
||||
|
||||
def datetime_or_none(dt):
|
||||
"""Validate a datetime or None value."""
|
||||
if dt is None or isinstance(dt, datetime.datetime):
|
||||
return dt
|
||||
raise ValueError('A datetime.datetime is required here')
|
||||
|
||||
|
||||
def int_or_none(val):
|
||||
"""Attempt to parse an integer value, or None."""
|
||||
if val is None:
|
||||
return val
|
||||
else:
|
||||
return int(val)
|
||||
|
||||
|
||||
def str_or_none(val):
|
||||
"""Attempt to stringify a value, or None."""
|
||||
if val is None:
|
||||
return val
|
||||
else:
|
||||
return str(val)
|
||||
|
||||
|
||||
def ip_or_none(version):
|
||||
"""Return a version-specific IP address validator."""
|
||||
def validator(val, version=version):
|
||||
if val is None:
|
||||
return val
|
||||
else:
|
||||
return netaddr.IPAddress(val, version=version)
|
||||
return validator
|
||||
|
||||
|
||||
def dt_serializer(name):
|
||||
"""Return a datetime serializer for a named attribute."""
|
||||
def serializer(self, name=name):
|
||||
if getattr(self, name) is not None:
|
||||
return timeutils.isotime(getattr(self, name))
|
||||
else:
|
||||
return None
|
||||
return serializer
|
||||
|
||||
|
||||
def dt_deserializer(instance, val):
|
||||
"""A deserializer method for datetime attributes."""
|
||||
if val is None:
|
||||
return None
|
||||
else:
|
||||
return timeutils.parse_isotime(val)
|
13
nova/tests/objects/__init__.py
Normal file
13
nova/tests/objects/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright 2013 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.
|
438
nova/tests/objects/test_objects.py
Normal file
438
nova/tests/objects/test_objects.py
Normal file
@ -0,0 +1,438 @@
|
||||
# Copyright 2013 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 datetime
|
||||
import gettext
|
||||
import netaddr
|
||||
|
||||
gettext.install('nova')
|
||||
|
||||
from nova.conductor import rpcapi as conductor_rpcapi
|
||||
from nova import context
|
||||
from nova import exception
|
||||
from nova.objects import base
|
||||
from nova.objects import utils
|
||||
from nova.openstack.common import timeutils
|
||||
from nova import test
|
||||
|
||||
|
||||
class MyObj(base.NovaObject):
|
||||
version = '1.5'
|
||||
fields = {'foo': int,
|
||||
'bar': str,
|
||||
'missing': str,
|
||||
}
|
||||
|
||||
def obj_load(self, attrname):
|
||||
setattr(self, attrname, 'loaded!')
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get(cls, context):
|
||||
obj = cls()
|
||||
obj.foo = 1
|
||||
obj.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
|
||||
|
||||
|
||||
class MyObj2(object):
|
||||
@classmethod
|
||||
def obj_name(cls):
|
||||
return 'MyObj'
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get(cls, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestMetaclass(test.TestCase):
|
||||
def test_obj_tracking(self):
|
||||
|
||||
class NewBaseClass(object):
|
||||
__metaclass__ = base.NovaObjectMetaclass
|
||||
fields = {}
|
||||
|
||||
@classmethod
|
||||
def obj_name(cls):
|
||||
return cls.__name__
|
||||
|
||||
class Test1(NewBaseClass):
|
||||
@staticmethod
|
||||
def obj_name():
|
||||
return 'fake1'
|
||||
|
||||
class Test2(NewBaseClass):
|
||||
pass
|
||||
|
||||
class Test2v2(NewBaseClass):
|
||||
@staticmethod
|
||||
def obj_name():
|
||||
return 'Test2'
|
||||
|
||||
expected = {'fake1': [Test1], 'Test2': [Test2, Test2v2]}
|
||||
|
||||
self.assertEqual(expected, NewBaseClass._obj_classes)
|
||||
# The following should work, also.
|
||||
self.assertEqual(expected, Test1._obj_classes)
|
||||
self.assertEqual(expected, Test2._obj_classes)
|
||||
|
||||
|
||||
class TestUtils(test.TestCase):
|
||||
def test_datetime_or_none(self):
|
||||
dt = datetime.datetime.now()
|
||||
self.assertEqual(utils.datetime_or_none(dt), dt)
|
||||
self.assertEqual(utils.datetime_or_none(None), None)
|
||||
self.assertRaises(ValueError, utils.datetime_or_none, 'foo')
|
||||
|
||||
def test_int_or_none(self):
|
||||
self.assertEqual(utils.int_or_none(1), 1)
|
||||
self.assertEqual(utils.int_or_none('1'), 1)
|
||||
self.assertEqual(utils.int_or_none(None), None)
|
||||
self.assertRaises(ValueError, utils.int_or_none, 'foo')
|
||||
|
||||
def test_str_or_none(self):
|
||||
class Obj(object):
|
||||
pass
|
||||
self.assertEqual(utils.str_or_none('foo'), 'foo')
|
||||
self.assertEqual(utils.str_or_none(1), '1')
|
||||
self.assertEqual(utils.str_or_none(None), None)
|
||||
|
||||
def test_ip_or_none(self):
|
||||
ip4 = netaddr.IPAddress('1.2.3.4', 4)
|
||||
ip6 = netaddr.IPAddress('1::2', 6)
|
||||
self.assertEqual(utils.ip_or_none(4)('1.2.3.4'), ip4)
|
||||
self.assertEqual(utils.ip_or_none(6)('1::2'), ip6)
|
||||
self.assertEqual(utils.ip_or_none(4)(None), None)
|
||||
self.assertEqual(utils.ip_or_none(6)(None), None)
|
||||
self.assertRaises(netaddr.AddrFormatError, utils.ip_or_none(4), 'foo')
|
||||
self.assertRaises(netaddr.AddrFormatError, utils.ip_or_none(6), 'foo')
|
||||
|
||||
def test_dt_serializer(self):
|
||||
class Obj(object):
|
||||
foo = utils.dt_serializer('bar')
|
||||
|
||||
obj = Obj()
|
||||
obj.bar = timeutils.parse_isotime('1955-11-05T00:00:00Z')
|
||||
self.assertEqual(obj.foo(), '1955-11-05T00:00:00Z')
|
||||
obj.bar = None
|
||||
self.assertEqual(obj.foo(), None)
|
||||
obj.bar = 'foo'
|
||||
self.assertRaises(AttributeError, obj.foo)
|
||||
|
||||
def test_dt_deserializer(self):
|
||||
dt = timeutils.parse_isotime('1955-11-05T00:00:00Z')
|
||||
self.assertEqual(utils.dt_deserializer(None, timeutils.isotime(dt)),
|
||||
dt)
|
||||
self.assertEqual(utils.dt_deserializer(None, None), None)
|
||||
self.assertRaises(ValueError, utils.dt_deserializer, None, 'foo')
|
||||
|
||||
|
||||
class _BaseTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(_BaseTestCase, self).setUp()
|
||||
self.remote_object_calls = list()
|
||||
|
||||
|
||||
class _LocalTest(_BaseTestCase):
|
||||
def setUp(self):
|
||||
super(_LocalTest, self).setUp()
|
||||
# Just in case
|
||||
base.NovaObject.indirection_api = None
|
||||
|
||||
def assertRemotes(self):
|
||||
self.assertEqual(self.remote_object_calls, [])
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def things_temporarily_local():
|
||||
# Temporarily go non-remote so the conductor handles
|
||||
# this request directly
|
||||
_api = base.NovaObject.indirection_api
|
||||
base.NovaObject.indirection_api = None
|
||||
yield
|
||||
base.NovaObject.indirection_api = _api
|
||||
|
||||
|
||||
class _RemoteTest(_BaseTestCase):
|
||||
def _testable_conductor(self):
|
||||
self.conductor_service = self.start_service(
|
||||
'conductor', manager='nova.conductor.manager.ConductorManager')
|
||||
self.remote_object_calls = list()
|
||||
|
||||
orig_object_class_action = \
|
||||
self.conductor_service.manager.object_class_action
|
||||
orig_object_action = \
|
||||
self.conductor_service.manager.object_action
|
||||
|
||||
def fake_object_class_action(*args, **kwargs):
|
||||
self.remote_object_calls.append((kwargs.get('objname'),
|
||||
kwargs.get('objmethod')))
|
||||
with things_temporarily_local():
|
||||
result = orig_object_class_action(*args, **kwargs)
|
||||
return result
|
||||
self.stubs.Set(self.conductor_service.manager, 'object_class_action',
|
||||
fake_object_class_action)
|
||||
|
||||
def fake_object_action(*args, **kwargs):
|
||||
self.remote_object_calls.append((kwargs.get('objinst'),
|
||||
kwargs.get('objmethod')))
|
||||
with things_temporarily_local():
|
||||
result = orig_object_action(*args, **kwargs)
|
||||
return result
|
||||
self.stubs.Set(self.conductor_service.manager, 'object_action',
|
||||
fake_object_action)
|
||||
|
||||
# Things are remoted by default in this session
|
||||
base.NovaObject.indirection_api = conductor_rpcapi.ConductorAPI()
|
||||
|
||||
def setUp(self):
|
||||
super(_RemoteTest, self).setUp()
|
||||
self._testable_conductor()
|
||||
|
||||
def assertRemotes(self):
|
||||
self.assertNotEqual(self.remote_object_calls, [])
|
||||
|
||||
|
||||
class _TestObject(object):
|
||||
def test_hydration_type_error(self):
|
||||
primitive = {'nova_object.name': 'MyObj',
|
||||
'nova_object.namespace': 'nova',
|
||||
'nova_object.version': '1.5',
|
||||
'nova_object.data': {'foo': 'a'}}
|
||||
self.assertRaises(ValueError, MyObj.obj_from_primitive, primitive)
|
||||
|
||||
def test_hydration(self):
|
||||
primitive = {'nova_object.name': 'MyObj',
|
||||
'nova_object.namespace': 'nova',
|
||||
'nova_object.version': '1.5',
|
||||
'nova_object.data': {'foo': 1}}
|
||||
obj = MyObj.obj_from_primitive(primitive)
|
||||
self.assertEqual(obj.foo, 1)
|
||||
|
||||
def test_hydration_bad_ns(self):
|
||||
primitive = {'nova_object.name': 'MyObj',
|
||||
'nova_object.namespace': 'foo',
|
||||
'nova_object.version': '1.5',
|
||||
'nova_object.data': {'foo': 1}}
|
||||
self.assertRaises(exception.UnsupportedObjectError,
|
||||
MyObj.obj_from_primitive, primitive)
|
||||
|
||||
def test_dehydration(self):
|
||||
expected = {'nova_object.name': 'MyObj',
|
||||
'nova_object.namespace': 'nova',
|
||||
'nova_object.version': '1.5',
|
||||
'nova_object.data': {'foo': 1}}
|
||||
obj = MyObj()
|
||||
obj.foo = 1
|
||||
obj.obj_reset_changes()
|
||||
self.assertEqual(obj.obj_to_primitive(), expected)
|
||||
|
||||
def test_object_property(self):
|
||||
obj = MyObj()
|
||||
obj.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()
|
||||
obj.foo = 123
|
||||
obj.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_loaded_in_primitive(self):
|
||||
obj = MyObj()
|
||||
obj.foo = 1
|
||||
obj.obj_reset_changes()
|
||||
self.assertEqual(obj.bar, 'loaded!')
|
||||
expected = {'nova_object.name': 'MyObj',
|
||||
'nova_object.namespace': 'nova',
|
||||
'nova_object.version': '1.5',
|
||||
'nova_object.changes': ['bar'],
|
||||
'nova_object.data': {'foo': 1,
|
||||
'bar': 'loaded!'}}
|
||||
self.assertEqual(obj.obj_to_primitive(), expected)
|
||||
|
||||
def test_changes_in_primitive(self):
|
||||
obj = MyObj()
|
||||
obj.foo = 123
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
primitive = obj.obj_to_primitive()
|
||||
self.assertTrue('nova_object.changes' in 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_unknown_objtype(self):
|
||||
self.assertRaises(exception.UnsupportedObjectError,
|
||||
base.NovaObject.obj_class_from_name, 'foo', '1.0')
|
||||
|
||||
def test_with_alternate_context(self):
|
||||
ctxt1 = context.RequestContext('foo', 'foo')
|
||||
ctxt2 = context.RequestContext('bar', 'alternate')
|
||||
obj = MyObj.get(ctxt1)
|
||||
obj.update_test(ctxt2)
|
||||
self.assertEqual(obj.bar, 'alternate-context')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_orphaned_object(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj._context = None
|
||||
self.assertRaises(exception.OrphanedObjectError,
|
||||
obj.update_test)
|
||||
self.assertRemotes()
|
||||
|
||||
def test_changed_1(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj.foo = 123
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
obj.update_test(ctxt)
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo', 'bar']))
|
||||
self.assertEqual(obj.foo, 123)
|
||||
self.assertRemotes()
|
||||
|
||||
def test_changed_2(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj.foo = 123
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
obj.save(ctxt)
|
||||
self.assertEqual(obj.obj_what_changed(), set([]))
|
||||
self.assertEqual(obj.foo, 123)
|
||||
self.assertRemotes()
|
||||
|
||||
def test_changed_3(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj.foo = 123
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
obj.refresh(ctxt)
|
||||
self.assertEqual(obj.obj_what_changed(), set([]))
|
||||
self.assertEqual(obj.foo, 321)
|
||||
self.assertEqual(obj.bar, 'refreshed')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_changed_4(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj.bar = 'something'
|
||||
self.assertEqual(obj.obj_what_changed(), set(['bar']))
|
||||
obj.modify_save_modify(ctxt)
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
self.assertEqual(obj.foo, 42)
|
||||
self.assertEqual(obj.bar, 'meow')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_static_result(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
self.assertEqual(obj.bar, 'bar')
|
||||
result = obj.marco()
|
||||
self.assertEqual(result, 'polo')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_updates(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
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()
|
||||
obj.created_at = dt
|
||||
obj.updated_at = dt
|
||||
obj.deleted_at = None
|
||||
expected = {'nova_object.name': 'MyObj',
|
||||
'nova_object.namespace': 'nova',
|
||||
'nova_object.version': '1.5',
|
||||
'nova_object.changes':
|
||||
['created_at', 'deleted_at', 'updated_at'],
|
||||
'nova_object.data':
|
||||
{'created_at': timeutils.isotime(dt),
|
||||
'updated_at': timeutils.isotime(dt),
|
||||
'deleted_at': None}
|
||||
}
|
||||
self.assertEqual(obj.obj_to_primitive(), expected)
|
||||
|
||||
|
||||
class TestObject(_LocalTest, _TestObject):
|
||||
pass
|
||||
|
||||
|
||||
class TestRemoteObject(_RemoteTest, _TestObject):
|
||||
def test_major_version_mismatch(self):
|
||||
ctxt = context.get_admin_context()
|
||||
MyObj2.version = '2.0'
|
||||
self.assertRaises(exception.IncompatibleObjectVersion,
|
||||
MyObj2.get, ctxt)
|
||||
|
||||
def test_minor_version_greater(self):
|
||||
ctxt = context.get_admin_context()
|
||||
MyObj2.version = '1.6'
|
||||
self.assertRaises(exception.IncompatibleObjectVersion,
|
||||
MyObj2.get, ctxt)
|
||||
|
||||
def test_minor_version_less(self):
|
||||
ctxt = context.get_admin_context()
|
||||
MyObj2.version = '1.2'
|
||||
obj = MyObj2.get(ctxt)
|
||||
self.assertEqual(obj.bar, 'bar')
|
||||
self.assertRemotes()
|
Loading…
x
Reference in New Issue
Block a user