Import serialization and nesting from Nova Objects

This change mostly merges the following commits from Nova:

e91c3d141c957485dcb66c73e84b41b775e4268b
f1c4b8e5f34eb6b5e70da6711750dcf05cea8c0a
65f6c536fecd3c788b2e0dfa9d66ecd24ca550e1
92a3190128547403dc603e5a40e377c6eb0c8025
68cb4d53385821c3ffdc40c299a77d11a7f98f27

Change-Id: I0a16f45674f5d14f458e2bb490d909a9086ea8b4
This commit is contained in:
Devananda van der Veen
2013-07-17 12:21:23 -07:00
parent 3cabb052e1
commit a5e5bd7cee
4 changed files with 311 additions and 22 deletions

View File

@@ -37,10 +37,10 @@ def make_class_properties(cls):
cls.fields.update(IronicObject.fields) cls.fields.update(IronicObject.fields)
for name, typefn in cls.fields.iteritems(): for name, typefn in cls.fields.iteritems():
def getter(self, name=name, typefn=typefn): def getter(self, name=name):
attrname = get_attrname(name) attrname = get_attrname(name)
if not hasattr(self, attrname): if not hasattr(self, attrname):
self.obj_load(name) self.obj_load_attr(name)
return getattr(self, attrname) return getattr(self, attrname)
def setter(self, value, name=name, typefn=typefn): def setter(self, value, name=name, typefn=typefn):
@@ -175,9 +175,10 @@ class IronicObject(object):
# by subclasses, but that is a special case. Objects inheriting from # by subclasses, but that is a special case. Objects inheriting from
# other objects will not receive this merging of fields contents. # other objects will not receive this merging of fields contents.
fields = { fields = {
'created_at': obj_utils.datetime_or_none, 'created_at': obj_utils.datetime_or_str_or_none,
'updated_at': obj_utils.datetime_or_none, 'updated_at': obj_utils.datetime_or_str_or_none,
} }
obj_extra_fields = []
def __init__(self): def __init__(self):
self._changed_fields = set() self._changed_fields = set()
@@ -216,7 +217,6 @@ class IronicObject(object):
_attr_created_at_from_primitive = obj_utils.dt_deserializer _attr_created_at_from_primitive = obj_utils.dt_deserializer
_attr_updated_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): def _attr_from_primitive(self, attribute, value):
"""Attribute deserialization dispatcher. """Attribute deserialization dispatcher.
@@ -231,7 +231,7 @@ class IronicObject(object):
return value return value
@classmethod @classmethod
def obj_from_primitive(cls, primitive): def obj_from_primitive(cls, primitive, context=None):
"""Simple base-case hydration. """Simple base-case hydration.
This calls self._attr_from_primitive() for each item in fields. This calls self._attr_from_primitive() for each item in fields.
@@ -247,6 +247,7 @@ class IronicObject(object):
objdata = primitive['ironic_object.data'] objdata = primitive['ironic_object.data']
objclass = cls.obj_class_from_name(objname, objver) objclass = cls.obj_class_from_name(objname, objver)
self = objclass() self = objclass()
self._context = context
for name in self.fields: for name in self.fields:
if name in objdata: if name in objdata:
setattr(self, name, setattr(self, name,
@@ -257,7 +258,6 @@ class IronicObject(object):
_attr_created_at_to_primitive = obj_utils.dt_serializer('created_at') _attr_created_at_to_primitive = obj_utils.dt_serializer('created_at')
_attr_updated_at_to_primitive = obj_utils.dt_serializer('updated_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): def _attr_to_primitive(self, attribute):
"""Attribute serialization dispatcher. """Attribute serialization dispatcher.
@@ -307,7 +307,7 @@ class IronicObject(object):
raise NotImplementedError('Cannot save anything in the base class') raise NotImplementedError('Cannot save anything in the base class')
def obj_what_changed(self): def obj_what_changed(self):
"""Returns a list of fields that have been modified.""" """Returns a set of fields that have been modified."""
return self._changed_fields return self._changed_fields
def obj_reset_changes(self, fields=None): def obj_reset_changes(self, fields=None):
@@ -326,8 +326,9 @@ class IronicObject(object):
NOTE(danms): May be removed in the future. NOTE(danms): May be removed in the future.
""" """
for name in self.fields: for name in self.fields.keys() + self.obj_extra_fields:
if hasattr(self, get_attrname(name)): if (hasattr(self, get_attrname(name)) or
name in self.obj_extra_fields):
yield name, getattr(self, name) yield name, getattr(self, name)
items = lambda self: list(self.iteritems()) items = lambda self: list(self.iteritems())
@@ -346,6 +347,13 @@ class IronicObject(object):
""" """
setattr(self, name, value) setattr(self, name, value)
def __contains__(self, name):
"""For backwards-compatibility with dict-based objects.
NOTE(danms): May be removed in the future.
"""
return hasattr(self, get_attrname(name))
def get(self, key, value=None): def get(self, key, value=None):
"""For backwards-compatibility with dict-based objects. """For backwards-compatibility with dict-based objects.
@@ -353,6 +361,71 @@ class IronicObject(object):
""" """
return self[key] return 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():
self[key] = value
class ObjectListBase(object):
"""Mixin class for lists of objects.
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': list,
}
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 an IronicObject!
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 _attr_objects_to_primitive(self):
"""Serialization of object list."""
return [x.obj_to_primitive() for x in self.objects]
def _attr_objects_from_primitive(self, value):
"""Deserialization of object list."""
objects = []
for entity in value:
obj = IronicObject.obj_from_primitive(entity,
context=self._context)
objects.append(obj)
return objects
class IronicObjectSerializer(rpc_serializer.Serializer): class IronicObjectSerializer(rpc_serializer.Serializer):
"""A IronicObject-aware Serializer. """A IronicObject-aware Serializer.
@@ -362,14 +435,53 @@ class IronicObjectSerializer(rpc_serializer.Serializer):
that needs to accept or return IronicObjects as arguments or result values that needs to accept or return IronicObjects as arguments or result values
should pass this to its RpcProxy and RpcDispatcher objects. should pass this to its RpcProxy and RpcDispatcher objects.
""" """
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 iterable == set:
# 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.
iterable = tuple
return iterable([action_fn(context, value) for value in values])
def serialize_entity(self, context, entity): def serialize_entity(self, context, entity):
if (hasattr(entity, 'obj_to_primitive') and if isinstance(entity, (tuple, list, set)):
callable(entity.obj_to_primitive)): 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() entity = entity.obj_to_primitive()
return entity return entity
def deserialize_entity(self, context, entity): def deserialize_entity(self, context, entity):
if isinstance(entity, dict) and 'ironic_object.name' in entity: if isinstance(entity, dict) and 'ironic_object.name' in entity:
entity = IronicObject.obj_from_primitive(entity) entity = IronicObject.obj_from_primitive(entity, context=context)
entity._context = context elif isinstance(entity, (tuple, list, set)):
entity = self._process_iterable(context, self.deserialize_entity,
entity)
return entity return entity
def obj_to_primitive(obj):
"""Recursively turn an object into a python primitive.
An IronicObject 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, IronicObject):
result = {}
for key, value in obj.iteritems():
result[key] = obj_to_primitive(value)
return result
else:
return obj

View File

@@ -16,6 +16,7 @@
import ast import ast
import datetime import datetime
import iso8601
import netaddr import netaddr
from ironic.openstack.common import timeutils from ironic.openstack.common import timeutils
@@ -23,11 +24,25 @@ from ironic.openstack.common import timeutils
def datetime_or_none(dt): def datetime_or_none(dt):
"""Validate a datetime or None value.""" """Validate a datetime or None value."""
if dt is None or isinstance(dt, datetime.datetime): if dt is None:
return dt return None
elif isinstance(dt, datetime.datetime):
if dt.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.
return dt.replace(tzinfo=iso8601.iso8601.Utc())
else:
return dt
raise ValueError('A datetime.datetime is required here') raise ValueError('A datetime.datetime is required here')
def datetime_or_str_or_none(val):
if isinstance(val, basestring):
return timeutils.parse_isotime(val)
return datetime_or_none(val)
def int_or_none(val): def int_or_none(val):
"""Attempt to parse an integer value, or None.""" """Attempt to parse an integer value, or None."""
if val is None: if val is None:
@@ -67,6 +82,14 @@ def ip_or_none(version):
return validator return validator
def nested_object_or_none(objclass):
def validator(val, objclass=objclass):
if val is None or isinstance(val, objclass):
return val
raise ValueError('An object of class %s is required here' % objclass)
return validator
def dt_serializer(name): def dt_serializer(name):
"""Return a datetime serializer for a named attribute.""" """Return a datetime serializer for a named attribute."""
def serializer(self, name=name): def serializer(self, name=name):
@@ -83,3 +106,12 @@ def dt_deserializer(instance, val):
return None return None
else: else:
return timeutils.parse_isotime(val) return timeutils.parse_isotime(val)
def obj_serializer(name):
def serializer(self, name=name):
if getattr(self, name) is not None:
return getattr(self, name).obj_to_primitive()
else:
return None
return serializer

View File

@@ -26,6 +26,7 @@ inline callbacks.
import eventlet import eventlet
eventlet.monkey_patch(os=False) eventlet.monkey_patch(os=False)
import copy
import os import os
import shutil import shutil
import sys import sys
@@ -40,6 +41,7 @@ from oslo.config import cfg
from ironic.db import migration from ironic.db import migration
from ironic.common import paths from ironic.common import paths
from ironic.objects import base as objects_base
from ironic.openstack.common.db.sqlalchemy import session from ironic.openstack.common.db.sqlalchemy import session
from ironic.openstack.common import log as logging from ironic.openstack.common import log as logging
from ironic.openstack.common import timeutils from ironic.openstack.common import timeutils
@@ -182,6 +184,14 @@ class TestCase(testtools.TestCase):
sqlite_clean_db=CONF.sqlite_clean_db) sqlite_clean_db=CONF.sqlite_clean_db)
self.useFixture(_DB_CACHE) self.useFixture(_DB_CACHE)
# NOTE(danms): Make sure to reset us back to non-remote objects
# for each test to avoid interactions. Also, backup the object
# registry
objects_base.IronicObject.indirection_api = None
self._base_test_obj_backup = copy.copy(
objects_base.IronicObject._obj_classes)
self.addCleanup(self._restore_obj_registry)
mox_fixture = self.useFixture(MoxStubout()) mox_fixture = self.useFixture(MoxStubout())
self.mox = mox_fixture.mox self.mox = mox_fixture.mox
self.stubs = mox_fixture.stubs self.stubs = mox_fixture.stubs
@@ -190,6 +200,9 @@ class TestCase(testtools.TestCase):
self.policy = self.useFixture(policy_fixture.PolicyFixture()) self.policy = self.useFixture(policy_fixture.PolicyFixture())
CONF.set_override('fatal_exception_format_errors', True) CONF.set_override('fatal_exception_format_errors', True)
def _restore_obj_registry(self):
objects_base.IronicObject._obj_classes = self._base_test_obj_backup
def _clear_attrs(self): def _clear_attrs(self):
# Delete attributes that don't start with _ so they don't pin # Delete attributes that don't start with _ so they don't pin
# memory around unnecessarily for the duration of the test # memory around unnecessarily for the duration of the test

View File

@@ -15,6 +15,7 @@
import contextlib import contextlib
import datetime import datetime
import gettext import gettext
import iso8601
import netaddr import netaddr
gettext.install('ironic') gettext.install('ironic')
@@ -24,7 +25,7 @@ from ironic.objects import base
from ironic.objects import utils from ironic.objects import utils
from ironic.openstack.common import context from ironic.openstack.common import context
from ironic.openstack.common import timeutils from ironic.openstack.common import timeutils
from ironic.tests.db import base as test_base from ironic.tests import base as test_base
class MyObj(base.IronicObject): class MyObj(base.IronicObject):
@@ -34,7 +35,7 @@ class MyObj(base.IronicObject):
'missing': str, 'missing': str,
} }
def obj_load(self, attrname): def obj_load_attr(self, attrname):
setattr(self, attrname, 'loaded!') setattr(self, attrname, 'loaded!')
@base.remotable_classmethod @base.remotable_classmethod
@@ -83,7 +84,7 @@ class MyObj2(object):
pass pass
class TestMetaclass(test_base.DbTestCase): class TestMetaclass(test_base.TestCase):
def test_obj_tracking(self): def test_obj_tracking(self):
class NewBaseClass(object): class NewBaseClass(object):
@@ -115,13 +116,25 @@ class TestMetaclass(test_base.DbTestCase):
self.assertEqual(expected, Test2._obj_classes) self.assertEqual(expected, Test2._obj_classes)
class TestUtils(test_base.DbTestCase): class TestUtils(test_base.TestCase):
def test_datetime_or_none(self): def test_datetime_or_none(self):
dt = datetime.datetime.now() naive_dt = datetime.datetime.now()
dt = timeutils.parse_isotime(timeutils.isotime(naive_dt))
self.assertEqual(utils.datetime_or_none(dt), dt) self.assertEqual(utils.datetime_or_none(dt), dt)
self.assertEqual(utils.datetime_or_none(dt),
naive_dt.replace(tzinfo=iso8601.iso8601.Utc(),
microsecond=0))
self.assertEqual(utils.datetime_or_none(None), None) self.assertEqual(utils.datetime_or_none(None), None)
self.assertRaises(ValueError, utils.datetime_or_none, 'foo') self.assertRaises(ValueError, utils.datetime_or_none, 'foo')
def test_datetime_or_str_or_none(self):
dts = timeutils.isotime()
dt = timeutils.parse_isotime(dts)
self.assertEqual(utils.datetime_or_str_or_none(dt), dt)
self.assertEqual(utils.datetime_or_str_or_none(None), None)
self.assertEqual(utils.datetime_or_str_or_none(dts), dt)
self.assertRaises(ValueError, utils.datetime_or_str_or_none, 'foo')
def test_int_or_none(self): 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('1'), 1) self.assertEqual(utils.int_or_none('1'), 1)
@@ -164,8 +177,33 @@ class TestUtils(test_base.DbTestCase):
self.assertEqual(utils.dt_deserializer(None, None), None) self.assertEqual(utils.dt_deserializer(None, None), None)
self.assertRaises(ValueError, utils.dt_deserializer, None, 'foo') self.assertRaises(ValueError, utils.dt_deserializer, None, 'foo')
def test_obj_to_primitive_list(self):
class MyList(base.ObjectListBase, base.IronicObject):
pass
mylist = MyList()
mylist.objects = [1, 2, 3]
self.assertEqual([1, 2, 3], base.obj_to_primitive(mylist))
class _BaseTestCase(test_base.DbTestCase): def test_obj_to_primitive_dict(self):
myobj = MyObj()
myobj.foo = 1
myobj.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.IronicObject):
pass
mylist = 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 _BaseTestCase(test_base.TestCase):
def setUp(self): def setUp(self):
super(_BaseTestCase, self).setUp() super(_BaseTestCase, self).setUp()
self.remote_object_calls = list() self.remote_object_calls = list()
@@ -251,6 +289,19 @@ class _TestObject(object):
obj = MyObj() obj = MyObj()
self.assertEqual(obj.bar, 'loaded!') self.assertEqual(obj.bar, 'loaded!')
def test_load_in_base(self):
class Foo(base.IronicObject):
fields = {'foobar': int}
obj = Foo()
# NOTE(danms): Can't use assertRaisesRegexp() because of py26
raised = False
try:
obj.foobar
except NotImplementedError as ex:
raised = True
self.assertTrue(raised)
self.assertTrue('foobar' in str(ex))
def test_loaded_in_primitive(self): def test_loaded_in_primitive(self):
obj = MyObj() obj = MyObj()
obj.foo = 1 obj.foo = 1
@@ -370,6 +421,87 @@ class _TestObject(object):
} }
self.assertEqual(obj.obj_to_primitive(), expected) self.assertEqual(obj.obj_to_primitive(), expected)
def test_contains(self):
obj = MyObj()
self.assertFalse('foo' in obj)
obj.foo = 1
self.assertTrue('foo' in obj)
self.assertFalse('does_not_exist' in obj)
class TestObject(_LocalTest, _TestObject): class TestObject(_LocalTest, _TestObject):
pass pass
class TestObjectListBase(test_base.TestCase):
def test_list_like_operations(self):
class Foo(base.ObjectListBase, base.IronicObject):
pass
objlist = Foo()
objlist._context = 'foo'
objlist.objects = [1, 2, 3]
self.assertTrue(list(objlist), objlist.objects)
self.assertEqual(len(objlist), 3)
self.assertIn(2, objlist)
self.assertEqual(list(objlist[:1]), [1])
self.assertEqual(objlist[:1]._context, 'foo')
self.assertEqual(objlist[2], 3)
self.assertEqual(objlist.count(1), 1)
self.assertEqual(objlist.index(2), 1)
def test_serialization(self):
class Foo(base.ObjectListBase, base.IronicObject):
pass
class Bar(base.IronicObject):
fields = {'foo': str}
obj = Foo()
obj.objects = []
for i in 'abc':
bar = Bar()
bar.foo = i
obj.objects.append(bar)
obj2 = base.IronicObject.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])
class TestObjectSerializer(test_base.TestCase):
def test_serialize_entity_primitive(self):
ser = base.IronicObjectSerializer()
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.IronicObjectSerializer()
for thing in (1, 'foo', [1, 2], {'foo': 'bar'}):
self.assertEqual(thing, ser.deserialize_entity(None, thing))
def test_object_serialization(self):
ser = base.IronicObjectSerializer()
ctxt = context.get_admin_context()
obj = MyObj()
primitive = ser.serialize_entity(ctxt, obj)
self.assertTrue('ironic_object.name' in primitive)
obj2 = ser.deserialize_entity(ctxt, primitive)
self.assertTrue(isinstance(obj2, MyObj))
self.assertEqual(ctxt, obj2._context)
def test_object_serialization_iterables(self):
ser = base.IronicObjectSerializer()
ctxt = context.get_admin_context()
obj = MyObj()
for iterable in (list, tuple, set):
thing = iterable([obj])
primitive = ser.serialize_entity(ctxt, thing)
self.assertEqual(1, len(primitive))
for item in primitive:
self.assertFalse(isinstance(item, base.IronicObject))
thing2 = ser.deserialize_entity(ctxt, primitive)
self.assertEqual(1, len(thing2))
for item in thing2:
self.assertTrue(isinstance(item, MyObj))