diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 988480ac94..cd528e7f4b 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -347,10 +347,6 @@ class SSHCommandFailed(IronicException): message = _("Failed to execute command via SSH: %(cmd)s.") -class UnsupportedObjectError(IronicException): - message = _('Unsupported object type %(objtype)s') - - class OrphanedObjectError(IronicException): message = _('Cannot call %(method)s on orphaned %(objtype)s object') diff --git a/ironic/objects/base.py b/ironic/objects/base.py index aa926894f5..6ea476fd5b 100644 --- a/ironic/objects/base.py +++ b/ironic/objects/base.py @@ -14,28 +14,18 @@ """Ironic common internal object model""" -import copy - from oslo_context import context from oslo_log import log as logging -from oslo_utils import versionutils from oslo_versionedobjects import base as object_base from ironic.common import exception from ironic.common.i18n import _ -from ironic.common.i18n import _LE from ironic.objects import fields as object_fields -from ironic.objects import utils as obj_utils LOG = logging.getLogger('object') -def get_attrname(name): - """Return the mangled name of the attribute's underlying storage.""" - return '_obj_' + name - - class IronicObjectRegistry(object_base.VersionedObjectRegistry): pass @@ -84,7 +74,8 @@ def remotable(fn): ctxt, self, fn.__name__, args, kwargs) for key, value in updates.items(): if key in self.fields: - self[key] = self._attr_from_primitive(key, value) + field = self.fields[key] + self[key] = field.from_primitive(self, key, value) self._changed_fields = set(updates.get('obj_what_changed', [])) return result else: @@ -116,7 +107,7 @@ def check_object_version(server, client): dict(client=client_minor, server=server_minor)) -class IronicObject(object_base.VersionedObjectDictCompat): +class IronicObject(object_base.VersionedObject): """Base class and object factory. This forms the base of all objects that can be remoted or instantiated @@ -126,226 +117,15 @@ class IronicObject(object_base.VersionedObjectDictCompat): as appropriate. """ - indirection_api = None - OBJ_SERIAL_NAMESPACE = 'ironic_object' - # Version of this object (see rules above check_object_version()) - VERSION = '1.0' + OBJ_PROJECT_NAMESPACE = 'ironic' - # 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 IronicObject 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. + # TODO(lintan) Refactor these fields and create PersistentObject and + # TimeStampObject like Nova when it is necessary. fields = { 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), } - obj_extra_fields = [] - - _attr_created_at_from_primitive = obj_utils.dt_deserializer - _attr_updated_at_from_primitive = obj_utils.dt_deserializer - _attr_created_at_to_primitive = obj_utils.dt_serializer('created_at') - _attr_updated_at_to_primitive = obj_utils.dt_serializer('updated_at') - - def __init__(self, context, **kwargs): - self._changed_fields = set() - self._context = context - self.update(kwargs) - - @classmethod - def obj_name(cls): - """Get canonical object name. - - This object name will be used over the wire for remote hydration. - """ - return cls.__name__ - - @classmethod - def obj_class_from_name(cls, objname, objver): - """Returns a class from the registry based on a name and version.""" - if objname not in IronicObjectRegistry.obj_classes(): - LOG.error(_LE('Unable to instantiate unregistered object type ' - '%(objtype)s'), dict(objtype=objname)) - raise exception.UnsupportedObjectError(objtype=objname) - - latest = None - compatible_match = None - obj_classes = IronicObjectRegistry.obj_classes() - for objclass in obj_classes[objname]: - if objclass.VERSION == objver: - return objclass - - version_bits = tuple([int(x) for x in objclass.VERSION.split(".")]) - if latest is None: - latest = version_bits - elif latest < version_bits: - latest = version_bits - - if versionutils.is_compatible(objver, objclass.VERSION): - compatible_match = objclass - - if compatible_match: - return compatible_match - - latest_ver = '%i.%i' % latest - raise exception.IncompatibleObjectVersion(objname=objname, - objver=objver, - supported=latest_ver) - - 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, context, objver, primitive): - self = cls(context) - self.VERSION = objver - objdata = primitive['ironic_object.data'] - changes = primitive.get('ironic_object.changes', []) - for name in self.fields: - if name in objdata: - setattr(self, name, - self._attr_from_primitive(name, objdata[name])) - self._changed_fields = set([x for x in changes if x in self.fields]) - return self - - @classmethod - def obj_from_primitive(cls, primitive, context=None): - """Simple base-case hydration. - - This calls self._attr_from_primitive() for each item in fields. - """ - if primitive['ironic_object.namespace'] != 'ironic': - # NOTE(danms): We don't do anything with this now, but it's - # there for "the future" - raise exception.UnsupportedObjectError( - objtype='%s.%s' % (primitive['ironic_object.namespace'], - primitive['ironic_object.name'])) - objname = primitive['ironic_object.name'] - objver = primitive['ironic_object.version'] - objclass = cls.obj_class_from_name(objname, objver) - return objclass._obj_from_primitive(context, objver, primitive) - - def __deepcopy__(self, memo): - """Efficiently make a deep copy of this object.""" - - # NOTE(danms): A naive deepcopy would copy more than we need, - # and since we have knowledge of the volatile bits of the - # object, we can be smarter here. Also, nested entities within - # some objects may be uncopyable, so we can avoid those sorts - # of issues by copying only our field data. - - nobj = self.__class__(self._context) - for name in self.fields: - if self.obj_attr_is_set(name): - nval = copy.deepcopy(getattr(self, name), memo) - setattr(nobj, name, nval) - nobj._changed_fields = set(self._changed_fields) - return nobj - - def obj_clone(self): - """Create a copy.""" - return copy.deepcopy(self) - - def _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 = {'ironic_object.name': self.obj_name(), - 'ironic_object.namespace': 'ironic', - 'ironic_object.version': self.VERSION, - 'ironic_object.data': primitive} - if self.obj_what_changed(): - obj['ironic_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") % - {'attrname': attrname}) - - def save(self, context): - """Save the changed fields back to the store. - - This is optional for subclasses, but is presented here in the base - class for consistency among those that do. - """ - raise NotImplementedError(_("Cannot save anything in the base class")) - - def obj_get_changes(self): - """Returns a dict of changed fields and their new values.""" - changes = {} - for key in self.obj_what_changed(): - changes[key] = self[key] - return changes - - def obj_what_changed(self): - """Returns a set 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() - - def obj_attr_is_set(self, attrname): - """Test object to see if attrname is present. - - Returns True if the named attribute has a value set, or - False if not. Raises AttributeError if attrname is not - a valid attribute for this object. - """ - if attrname not in self.obj_fields: - raise AttributeError( - _("%(objname)s object has no attribute '%(attrname)s'") % - {'objname': self.obj_name(), 'attrname': attrname}) - return hasattr(self, get_attrname(attrname)) - - @property - def obj_fields(self): - return list(self.fields) + self.obj_extra_fields def as_dict(self): return dict((k, getattr(self, k)) @@ -360,7 +140,7 @@ class IronicObject(object_base.VersionedObjectDictCompat): object. """ for field in self.fields: - if (hasattr(self, get_attrname(field)) and + if (self.obj_attr_is_set(field) and self[field] != loaded_object[field]): self[field] = loaded_object[field] diff --git a/ironic/objects/chassis.py b/ironic/objects/chassis.py index 6e959dabee..08dd64f3a9 100644 --- a/ironic/objects/chassis.py +++ b/ironic/objects/chassis.py @@ -15,6 +15,7 @@ from oslo_utils import strutils from oslo_utils import uuidutils +from oslo_versionedobjects import base as object_base from ironic.common import exception from ironic.db import api as dbapi @@ -23,7 +24,7 @@ from ironic.objects import fields as object_fields @base.IronicObjectRegistry.register -class Chassis(base.IronicObject): +class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version # Version 1.1: Add get() and get_by_id() and make get_by_uuid() # only work with a uuid diff --git a/ironic/objects/conductor.py b/ironic/objects/conductor.py index 2f4e9f1219..4fabe58a29 100644 --- a/ironic/objects/conductor.py +++ b/ironic/objects/conductor.py @@ -14,6 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_versionedobjects import base as object_base + from ironic.common.i18n import _ from ironic.db import api as db_api from ironic.objects import base @@ -21,7 +23,7 @@ from ironic.objects import fields as object_fields @base.IronicObjectRegistry.register -class Conductor(base.IronicObject): +class Conductor(base.IronicObject, object_base.VersionedObjectDictCompat): dbapi = db_api.get_instance() diff --git a/ironic/objects/node.py b/ironic/objects/node.py index 9ab9fae040..33dcab2521 100644 --- a/ironic/objects/node.py +++ b/ironic/objects/node.py @@ -15,6 +15,7 @@ from oslo_utils import strutils from oslo_utils import uuidutils +from oslo_versionedobjects import base as object_base from ironic.common import exception from ironic.db import api as db_api @@ -23,7 +24,7 @@ from ironic.objects import fields as object_fields @base.IronicObjectRegistry.register -class Node(base.IronicObject): +class Node(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version # Version 1.1: Added instance_info # Version 1.2: Add get() and get_by_id() and make get_by_uuid() diff --git a/ironic/objects/port.py b/ironic/objects/port.py index 3c50bf6bb2..f80095d418 100644 --- a/ironic/objects/port.py +++ b/ironic/objects/port.py @@ -15,6 +15,7 @@ from oslo_utils import strutils from oslo_utils import uuidutils +from oslo_versionedobjects import base as object_base from ironic.common import exception from ironic.common import utils @@ -24,7 +25,7 @@ from ironic.objects import fields as object_fields @base.IronicObjectRegistry.register -class Port(base.IronicObject): +class Port(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version # Version 1.1: Add get() and get_by_id() and get_by_address() and # make get_by_uuid() only work with a uuid diff --git a/ironic/tests/objects/test_objects.py b/ironic/tests/objects/test_objects.py index c4d54568cf..6e19481ebc 100644 --- a/ironic/tests/objects/test_objects.py +++ b/ironic/tests/objects/test_objects.py @@ -19,6 +19,9 @@ import iso8601 from oslo_context import context from oslo_utils import timeutils +from oslo_versionedobjects import base as object_base +from oslo_versionedobjects import exception as object_exception +import six from ironic.common import exception from ironic.objects import base @@ -30,7 +33,7 @@ gettext.install('ironic') @base.IronicObjectRegistry.register -class MyObj(base.IronicObject): +class MyObj(base.IronicObject, object_base.VersionedObjectDictCompat): VERSION = '1.5' fields = {'foo': fields.IntegerField(), @@ -167,7 +170,7 @@ class _TestObject(object): 'ironic_object.namespace': 'foo', 'ironic_object.version': '1.5', 'ironic_object.data': {'foo': 1}} - self.assertRaises(exception.UnsupportedObjectError, + self.assertRaises(object_exception.UnsupportedObjectError, MyObj.obj_from_primitive, primitive) def test_dehydration(self): @@ -207,7 +210,7 @@ class _TestObject(object): def test_load_in_base(self): @base.IronicObjectRegistry.register_if(False) - class Foo(base.IronicObject): + class Foo(base.IronicObject, object_base.VersionedObjectDictCompat): fields = {'foobar': fields.IntegerField()} obj = Foo(self.context) @@ -240,7 +243,7 @@ class _TestObject(object): self.assertEqual(set(), obj2.obj_what_changed()) def test_unknown_objtype(self): - self.assertRaises(exception.UnsupportedObjectError, + self.assertRaises(object_exception.UnsupportedObjectError, base.IronicObject.obj_class_from_name, 'foo', '1.0') def test_with_alternate_context(self): @@ -312,6 +315,7 @@ class _TestObject(object): def test_base_attributes(self): dt = datetime.datetime(1955, 11, 5, 0, 0, tzinfo=iso8601.iso8601.Utc()) + datatime = fields.DateTimeField() obj = MyObj(self.context) obj.created_at = dt obj.updated_at = dt @@ -321,8 +325,8 @@ class _TestObject(object): 'ironic_object.changes': ['created_at', 'updated_at'], 'ironic_object.data': - {'created_at': dt.isoformat(), - 'updated_at': dt.isoformat(), + {'created_at': datatime.stringify(dt), + 'updated_at': datatime.stringify(dt), } } actual = obj.obj_to_primitive() @@ -386,7 +390,8 @@ class _TestObject(object): def test_obj_fields(self): @base.IronicObjectRegistry.register_if(False) - class TestObj(base.IronicObject): + class TestObj(base.IronicObject, + object_base.VersionedObjectDictCompat): fields = {'foo': fields.IntegerField()} obj_extra_fields = ['bar'] @@ -400,7 +405,8 @@ class _TestObject(object): def test_refresh_object(self): @base.IronicObjectRegistry.register_if(False) - class TestObj(base.IronicObject): + class TestObj(base.IronicObject, + object_base.VersionedObjectDictCompat): fields = {'foo': fields.IntegerField(), 'bar': fields.StringField()} @@ -420,6 +426,21 @@ class _TestObject(object): self.assertEqual('abc', obj.bar) self.assertEqual(set(['foo', 'bar']), obj.obj_what_changed()) + def test_assign_value_without_DictCompat(self): + class TestObj(base.IronicObject): + fields = {'foo': fields.IntegerField(), + 'bar': fields.StringField()} + obj = TestObj(self.context) + obj.foo = 10 + err_message = '' + try: + obj['bar'] = 'value' + except TypeError as e: + err_message = six.text_type(e) + finally: + self.assertIn("'TestObj' object does not support item assignment", + err_message) + class TestObject(_LocalTest, _TestObject): pass