diff --git a/nova/objects/fields.py b/nova/objects/fields.py new file mode 100644 index 000000000000..3b243abcd289 --- /dev/null +++ b/nova/objects/fields.py @@ -0,0 +1,422 @@ +# Copyright 2013 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 +import six + +from nova.network import model as network_model +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import timeutils + + +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__, + }) + + +class AbstractFieldType(six.with_metaclass(abc.ABCMeta, 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 NovaObject 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 NovaObject 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 NovaObject 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 + + +class FieldType(AbstractFieldType): + def coerce(self, obj, attr, value): + return value + + def from_primitive(self, obj, attr, value): + return value + + def to_primitive(self, obj, attr, value): + return value + + def describe(self): + return self.__class__.__name__ + + +class UnspecifiedDefault(object): + pass + + +class Field(object): + def __init__(self, field_type, nullable=False, default=UnspecifiedDefault): + self._type = field_type + self._nullable = nullable + self._default = default + + @property + def nullable(self): + return self._nullable + + @property + def default(self): + return self._default + + 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 + + +class String(FieldType): + def coerce(self, obj, attr, value): + # FIXME(danms): We should really try to avoid the need to do this + if isinstance(value, (basestring, int, long, float, + datetime.datetime)): + return unicode(value) + else: + raise ValueError(_('A string is required here, not %s'), + value.__class__.__name__) + + +class UUID(FieldType): + def coerce(self, obj, attr, value): + # FIXME(danms): We should actually verify the UUIDness here + return str(value) + + +class Integer(FieldType): + def coerce(self, obj, attr, value): + return int(value) + + +class Boolean(FieldType): + def coerce(self, obj, attr, value): + return bool(value) + + +class DateTime(FieldType): + def coerce(self, obj, attr, value): + if isinstance(value, basestring): + value = timeutils.parse_isotime(value) + elif not isinstance(value, datetime.datetime): + raise ValueError(_('A datetime.datetime is required here')) + + if value.utcoffset() is None: + 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)) + + def to_primitive(self, obj, attr, value): + return timeutils.isotime(value) + + +class IPV4Address(FieldType): + def coerce(self, obj, attr, value): + try: + return netaddr.IPAddress(value, version=4) + except netaddr.AddrFormatError as e: + raise ValueError(str(e)) + + def from_primitive(self, obj, attr, value): + return self.coerce(obj, attr, value) + + def to_primitive(self, obj, attr, value): + return str(value) + + +class IPV6Address(FieldType): + def coerce(self, obj, attr, value): + try: + return netaddr.IPAddress(value, version=6) + except netaddr.AddrFormatError as e: + raise ValueError(str(e)) + + def from_primitive(self, obj, attr, value): + return self.coerce(obj, attr, value) + + def to_primitive(self, obj, attr, value): + return str(value) + + +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] + + +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, basestring): + raise KeyTypeError(basestring, 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 + + +class Object(FieldType): + def __init__(self, objtype, **kwargs): + self._objtype = objtype + super(Object, self).__init__(**kwargs) + + def coerce(self, obj, attr, value): + if not isinstance(value, self._objtype): + raise ValueError(_('An object of type %s is required here') % + self._objtype.obj_name()) + return value + + def to_primitive(self, obj, attr, value): + return value.obj_to_primitive() + + def from_primitive(self, obj, attr, value): + # FIXME(danms): Avoid circular import from base.py + from nova.objects import base as obj_base + return obj_base.NovaObject.obj_from_primitive(value, obj._context) + + def describe(self): + return "Object<%s>" % self._objtype.obj_name() + + +class NetworkModel(FieldType): + def coerce(self, obj, attr, value): + if isinstance(value, network_model.NetworkInfo): + return value + elif isinstance(value, basestring): + # Hmm, do we need this? + return network_model.NetworkInfo.hydrate(value) + else: + raise ValueError(_('A NetworkModel is required here')) + + def to_primitive(self, obj, attr, value): + return value.json() + + def from_primitive(self, obj, attr, value): + return network_model.NetworkInfo.hydrate(value) + + +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 BooleanField(AutoTypedField): + AUTO_TYPE = Boolean() + + +class DateTimeField(AutoTypedField): + AUTO_TYPE = DateTime() + + +class IPV4AddressField(AutoTypedField): + AUTO_TYPE = IPV4Address() + + +class IPV6AddressField(AutoTypedField): + AUTO_TYPE = IPV6Address() + + +class DictOfStringsField(AutoTypedField): + AUTO_TYPE = Dict(String()) + + +class DictOfNullableStringsField(AutoTypedField): + AUTO_TYPE = Dict(String(), nullable=True) + + +class ListOfStringsField(AutoTypedField): + AUTO_TYPE = List(String()) + + +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) diff --git a/nova/tests/objects/test_fields.py b/nova/tests/objects/test_fields.py new file mode 100644 index 000000000000..596c063a5817 --- /dev/null +++ b/nova/tests/objects/test_fields.py @@ -0,0 +1,214 @@ +# Copyright 2013 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 + +import netaddr + +from nova.network import model as network_model +from nova.objects import base as obj_base +from nova.objects import fields +from nova.openstack.common import timeutils +from nova 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.NoDBTestCase): + 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: + _context = 'context' + + for prim_val, out_val in self.from_primitive_values: + self.assertEqual(out_val, self.field.from_primitive( + ObjectLikeThing, 'attr', prim_val)) + + +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] + + +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 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)] + + +class TestIPAddressV4(TestField): + def setUp(self): + super(TestIPAddressV4, self).setUp() + self.field = fields.IPV4AddressField() + self.coerce_good_values = [('1.2.3.4', netaddr.IPAddress('1.2.3.4')), + (netaddr.IPAddress('1.2.3.4'), + netaddr.IPAddress('1.2.3.4'))] + self.coerce_bad_values = ['1-2', 'foo', '::1'] + self.to_primitive_values = [(netaddr.IPAddress('1.2.3.4'), '1.2.3.4')] + self.from_primitive_values = [('1.2.3.4', + netaddr.IPAddress('1.2.3.4'))] + + +class TestIPAddressV6(TestField): + def setUp(self): + super(TestIPAddressV6, self).setUp() + self.field = fields.IPV6AddressField() + self.coerce_good_values = [('::1', netaddr.IPAddress('::1')), + (netaddr.IPAddress('::1'), + netaddr.IPAddress('::1'))] + self.coerce_bad_values = ['1.2', 'foo', '1.2.3.4'] + self.to_primitive_values = [(netaddr.IPAddress('::1'), '::1')] + self.from_primitive_values = [('::1', + netaddr.IPAddress('::1'))] + + +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'})] + + +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'})] + + +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'})] + + +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'])] + + +class TestObject(TestField): + def setUp(self): + class TestableObject(obj_base.NovaObject): + 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.NovaObject): + pass + + test_inst = TestableObject() + super(TestObject, self).setUp() + 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)] + + +class TestNetworkModel(TestField): + def setUp(self): + super(TestNetworkModel, self).setUp() + model = network_model.NetworkInfo() + self.field = fields.Field(fields.NetworkModel()) + self.coerce_good_values = [(model, model), (model.json(), model)] + self.coerce_bad_values = [[], 'foo'] + self.to_primitive_values = [(model, model.json())] + self.from_primitive_values = [(model.json(), model)]