summaryrefslogtreecommitdiffstats
path: root/nova/objects
diff options
context:
space:
mode:
Diffstat (limited to 'nova/objects')
-rw-r--r--nova/objects/__init__.py13
-rw-r--r--nova/objects/base.py368
-rw-r--r--nova/objects/utils.py71
3 files changed, 452 insertions, 0 deletions
diff --git a/nova/objects/__init__.py b/nova/objects/__init__.py
new file mode 100644
index 000000000..67f4db51a
--- /dev/null
+++ b/nova/objects/__init__.py
@@ -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.
diff --git a/nova/objects/base.py b/nova/objects/base.py
new file mode 100644
index 000000000..abeebf990
--- /dev/null
+++ b/nova/objects/base.py
@@ -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
diff --git a/nova/objects/utils.py b/nova/objects/utils.py
new file mode 100644
index 000000000..042b7b36e
--- /dev/null
+++ b/nova/objects/utils.py
@@ -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)