Merge "Base IronicObject on VersionedObject"
This commit is contained in:
commit
83f7a1481a
@ -345,10 +345,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')
|
||||
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user