ironic/ironic/tests/unit/objects/test_objects.py

1005 lines
40 KiB
Python

# 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.
import contextlib
import datetime
import types
import iso8601
import mock
from oslo_utils import timeutils
from oslo_versionedobjects import base as object_base
from oslo_versionedobjects import exception as object_exception
from oslo_versionedobjects import fixture as object_fixture
import six
from ironic.common import context
from ironic.common import release_mappings
from ironic.conf import CONF
from ironic.objects import base
from ironic.objects import fields
from ironic.tests import base as test_base
@base.IronicObjectRegistry.register
class MyObj(base.IronicObject, object_base.VersionedObjectDictCompat):
VERSION = '1.5'
fields = {'foo': fields.IntegerField(),
'bar': fields.StringField(),
'missing': fields.StringField(),
# nested object added as string for simplicity
'nested_object': fields.StringField(),
}
def _set_from_db_object(self, context, db_object, fields=None):
fields = set(fields or self.fields) - {'nested_object'}
super(MyObj, self)._set_from_db_object(context, db_object, fields)
# some special manipulation with nested_object here
self['nested_object'] = db_object.get('nested_object', '') + 'test'
def obj_load_attr(self, attrname):
if attrname == 'version':
setattr(self, attrname, None)
else:
setattr(self, attrname, 'loaded!')
@object_base.remotable_classmethod
def query(cls, context):
obj = cls(context)
obj.foo = 1
obj.bar = 'bar'
obj.obj_reset_changes()
return obj
@object_base.remotable
def marco(self, context=None):
return 'polo'
@object_base.remotable
def update_test(self, context=None):
if context and context.tenant == 'alternate':
self.bar = 'alternate-context'
else:
self.bar = 'updated'
@object_base.remotable
def save(self, context=None):
self.do_version_changes_for_db()
self.obj_reset_changes()
@object_base.remotable
def refresh(self, context=None):
self.foo = 321
self.bar = 'refreshed'
self.obj_reset_changes()
@object_base.remotable
def modify_save_modify(self, context=None):
self.bar = 'meow'
self.save()
self.foo = 42
def _convert_to_version(self, target_version,
remove_unavailable_fields=True):
if target_version == '1.5':
self.missing = 'foo'
elif self.missing:
if remove_unavailable_fields:
delattr(self, 'missing')
else:
self.missing = ''
class MyObj2(object):
@classmethod
def obj_name(cls):
return 'MyObj'
@object_base.remotable_classmethod
def get(cls, *args, **kwargs):
pass
@base.IronicObjectRegistry.register_if(False)
class TestSubclassedObject(MyObj):
fields = {'new_field': fields.StringField()}
class _LocalTest(test_base.TestCase):
def setUp(self):
super(_LocalTest, self).setUp()
# Just in case
base.IronicObject.indirection_api = None
@contextlib.contextmanager
def things_temporarily_local():
# Temporarily go non-remote so the conductor handles
# this request directly
_api = base.IronicObject.indirection_api
base.IronicObject.indirection_api = None
yield
base.IronicObject.indirection_api = _api
class _TestObject(object):
def test_hydration_type_error(self):
primitive = {'ironic_object.name': 'MyObj',
'ironic_object.namespace': 'ironic',
'ironic_object.version': '1.5',
'ironic_object.data': {'foo': 'a'}}
self.assertRaises(ValueError, MyObj.obj_from_primitive, primitive)
def test_hydration(self):
primitive = {'ironic_object.name': 'MyObj',
'ironic_object.namespace': 'ironic',
'ironic_object.version': '1.5',
'ironic_object.data': {'foo': 1}}
obj = MyObj.obj_from_primitive(primitive)
self.assertEqual(1, obj.foo)
def test_hydration_bad_ns(self):
primitive = {'ironic_object.name': 'MyObj',
'ironic_object.namespace': 'foo',
'ironic_object.version': '1.5',
'ironic_object.data': {'foo': 1}}
self.assertRaises(object_exception.UnsupportedObjectError,
MyObj.obj_from_primitive, primitive)
def test_dehydration(self):
expected = {'ironic_object.name': 'MyObj',
'ironic_object.namespace': 'ironic',
'ironic_object.version': '1.5',
'ironic_object.data': {'foo': 1}}
obj = MyObj(self.context)
obj.foo = 1
obj.obj_reset_changes()
self.assertEqual(expected, obj.obj_to_primitive())
def test_get_updates(self):
obj = MyObj(self.context)
self.assertEqual({}, obj.obj_get_changes())
obj.foo = 123
self.assertEqual({'foo': 123}, obj.obj_get_changes())
obj.bar = 'test'
self.assertEqual({'foo': 123, 'bar': 'test'}, obj.obj_get_changes())
obj.obj_reset_changes()
self.assertEqual({}, obj.obj_get_changes())
def test_object_property(self):
obj = MyObj(self.context, foo=1)
self.assertEqual(1, obj.foo)
def test_object_property_type_error(self):
obj = MyObj(self.context)
def fail():
obj.foo = 'a'
self.assertRaises(ValueError, fail)
def test_load(self):
obj = MyObj(self.context)
self.assertEqual('loaded!', obj.bar)
def test_load_in_base(self):
@base.IronicObjectRegistry.register_if(False)
class Foo(base.IronicObject, object_base.VersionedObjectDictCompat):
fields = {'foobar': fields.IntegerField()}
obj = Foo(self.context)
self.assertRaisesRegex(
NotImplementedError, "Cannot load 'foobar' in the base class",
getattr, obj, 'foobar')
def test_loaded_in_primitive(self):
obj = MyObj(self.context)
obj.foo = 1
obj.obj_reset_changes()
self.assertEqual('loaded!', obj.bar)
expected = {'ironic_object.name': 'MyObj',
'ironic_object.namespace': 'ironic',
'ironic_object.version': '1.5',
'ironic_object.changes': ['bar'],
'ironic_object.data': {'foo': 1,
'bar': 'loaded!'}}
self.assertEqual(expected, obj.obj_to_primitive())
def test_changes_in_primitive(self):
obj = MyObj(self.context)
obj.foo = 123
self.assertEqual(set(['foo']), obj.obj_what_changed())
primitive = obj.obj_to_primitive()
self.assertIn('ironic_object.changes', primitive)
obj2 = MyObj.obj_from_primitive(primitive)
self.assertEqual(set(['foo']), obj2.obj_what_changed())
obj2.obj_reset_changes()
self.assertEqual(set(), obj2.obj_what_changed())
def test_unknown_objtype(self):
self.assertRaises(object_exception.UnsupportedObjectError,
base.IronicObject.obj_class_from_name, 'foo', '1.0')
def test_with_alternate_context(self):
ctxt1 = context.RequestContext(auth_token='foo', tenant='foo')
ctxt2 = context.RequestContext(auth_token='bar', tenant='alternate')
obj = MyObj.query(ctxt1)
obj.update_test(ctxt2)
self.assertEqual('alternate-context', obj.bar)
def test_orphaned_object(self):
obj = MyObj.query(self.context)
obj._context = None
self.assertRaises(object_exception.OrphanedObjectError,
obj.update_test)
def test_changed_1(self):
obj = MyObj.query(self.context)
obj.foo = 123
self.assertEqual(set(['foo']), obj.obj_what_changed())
obj.update_test(self.context)
self.assertEqual(set(['foo', 'bar']), obj.obj_what_changed())
self.assertEqual(123, obj.foo)
def test_changed_2(self):
obj = MyObj.query(self.context)
obj.foo = 123
self.assertEqual(set(['foo']), obj.obj_what_changed())
obj.save()
self.assertEqual(set([]), obj.obj_what_changed())
self.assertEqual(123, obj.foo)
def test_changed_3(self):
obj = MyObj.query(self.context)
obj.foo = 123
self.assertEqual(set(['foo']), obj.obj_what_changed())
obj.refresh()
self.assertEqual(set([]), obj.obj_what_changed())
self.assertEqual(321, obj.foo)
self.assertEqual('refreshed', obj.bar)
def test_changed_4(self):
obj = MyObj.query(self.context)
obj.bar = 'something'
self.assertEqual(set(['bar']), obj.obj_what_changed())
obj.modify_save_modify(self.context)
self.assertEqual(set(['foo']), obj.obj_what_changed())
self.assertEqual(42, obj.foo)
self.assertEqual('meow', obj.bar)
def test_static_result(self):
obj = MyObj.query(self.context)
self.assertEqual('bar', obj.bar)
result = obj.marco()
self.assertEqual('polo', result)
def test_updates(self):
obj = MyObj.query(self.context)
self.assertEqual(1, obj.foo)
obj.update_test()
self.assertEqual('updated', obj.bar)
def test_base_attributes(self):
dt = datetime.datetime(1955, 11, 5, 0, 0, tzinfo=iso8601.UTC)
datatime = fields.DateTimeField()
obj = MyObj(self.context)
obj.created_at = dt
obj.updated_at = dt
expected = {'ironic_object.name': 'MyObj',
'ironic_object.namespace': 'ironic',
'ironic_object.version': '1.5',
'ironic_object.changes':
['created_at', 'updated_at'],
'ironic_object.data':
{'created_at': datatime.stringify(dt),
'updated_at': datatime.stringify(dt),
}
}
actual = obj.obj_to_primitive()
# ironic_object.changes is built from a set and order is undefined
self.assertEqual(sorted(expected['ironic_object.changes']),
sorted(actual['ironic_object.changes']))
del expected['ironic_object.changes'], actual['ironic_object.changes']
self.assertEqual(expected, actual)
def test_contains(self):
obj = MyObj(self.context)
self.assertNotIn('foo', obj)
obj.foo = 1
self.assertIn('foo', obj)
self.assertNotIn('does_not_exist', obj)
def test_obj_attr_is_set(self):
obj = MyObj(self.context, foo=1)
self.assertTrue(obj.obj_attr_is_set('foo'))
self.assertFalse(obj.obj_attr_is_set('bar'))
self.assertRaises(AttributeError, obj.obj_attr_is_set, 'bang')
def test_get(self):
obj = MyObj(self.context, foo=1)
# Foo has value, should not get the default
self.assertEqual(1, obj.get('foo', 2))
# Foo has value, should return the value without error
self.assertEqual(1, obj.get('foo'))
# Bar is not loaded, so we should get the default
self.assertEqual('not-loaded', obj.get('bar', 'not-loaded'))
# Bar without a default should lazy-load
self.assertEqual('loaded!', obj.get('bar'))
# Bar now has a default, but loaded value should be returned
self.assertEqual('loaded!', obj.get('bar', 'not-loaded'))
# Invalid attribute should raise AttributeError
self.assertRaises(AttributeError, obj.get, 'nothing')
# ...even with a default
self.assertRaises(AttributeError, obj.get, 'nothing', 3)
def test_object_inheritance(self):
base_fields = list(base.IronicObject.fields)
myobj_fields = ['foo', 'bar', 'missing', 'nested_object'] + base_fields
myobj3_fields = ['new_field']
self.assertTrue(issubclass(TestSubclassedObject, MyObj))
self.assertEqual(len(myobj_fields), len(MyObj.fields))
self.assertEqual(set(myobj_fields), set(MyObj.fields.keys()))
self.assertEqual(len(myobj_fields) + len(myobj3_fields),
len(TestSubclassedObject.fields))
self.assertEqual(set(myobj_fields) | set(myobj3_fields),
set(TestSubclassedObject.fields.keys()))
def _test_get_changes(self, target_version='1.5'):
obj = MyObj(self.context)
self.assertEqual('1.5', obj.VERSION)
self.assertEqual(target_version, obj.get_target_version())
self.assertEqual({}, obj.obj_get_changes())
obj.foo = 123
self.assertEqual({'foo': 123}, obj.obj_get_changes())
obj.bar = 'test'
obj.missing = 'test' # field which is missing in v1.4
self.assertEqual({'foo': 123, 'bar': 'test', 'missing': 'test'},
obj.obj_get_changes())
obj.obj_reset_changes()
self.assertEqual({}, obj.obj_get_changes())
def test_get_changes(self):
self._test_get_changes()
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def test_get_changes_pinned(self, mock_release_mapping):
# obj_get_changes() is not affected by pinning
CONF.set_override('pin_release_version',
release_mappings.RELEASE_VERSIONS[-1])
mock_release_mapping.__getitem__.return_value = {
'objects': {
'MyObj': ['1.4'],
}
}
self._test_get_changes(target_version='1.4')
def test_convert_to_version_same(self):
# no changes
obj = MyObj(self.context)
self.assertEqual('1.5', obj.VERSION)
obj.convert_to_version('1.5', remove_unavailable_fields=False)
self.assertEqual('1.5', obj.VERSION)
self.assertEqual(obj.__class__.VERSION, obj.VERSION)
self.assertEqual({}, obj.obj_get_changes())
def test_convert_to_version_new(self):
obj = MyObj(self.context)
obj.VERSION = '1.4'
obj.convert_to_version('1.5', remove_unavailable_fields=False)
self.assertEqual('1.5', obj.VERSION)
self.assertEqual(obj.__class__.VERSION, obj.VERSION)
self.assertEqual({'missing': 'foo'}, obj.obj_get_changes())
def test_convert_to_version_old(self):
obj = MyObj(self.context)
obj.missing = 'something'
obj.obj_reset_changes()
obj.convert_to_version('1.4', remove_unavailable_fields=True)
self.assertEqual('1.4', obj.VERSION)
self.assertEqual({}, obj.obj_get_changes())
def test_convert_to_version_old_keep(self):
obj = MyObj(self.context)
obj.missing = 'something'
obj.obj_reset_changes()
obj.convert_to_version('1.4', remove_unavailable_fields=False)
self.assertEqual('1.4', obj.VERSION)
self.assertEqual({'missing': ''}, obj.obj_get_changes())
@mock.patch.object(MyObj, 'convert_to_version', autospec=True)
def test_do_version_changes_for_db(self, mock_convert):
# no object conversion
obj = MyObj(self.context)
self.assertEqual('1.5', obj.VERSION)
self.assertEqual('1.5', obj.get_target_version())
self.assertEqual({}, obj.obj_get_changes())
obj.foo = 123
obj.bar = 'test'
obj.missing = 'test' # field which is missing in v1.4
self.assertEqual({'foo': 123, 'bar': 'test', 'missing': 'test'},
obj.obj_get_changes())
changes = obj.do_version_changes_for_db()
self.assertEqual({'foo': 123, 'bar': 'test', 'missing': 'test',
'version': '1.5'}, changes)
self.assertEqual('1.5', obj.VERSION)
self.assertFalse(mock_convert.called)
@mock.patch.object(MyObj, 'convert_to_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version',
spec_set=types.FunctionType)
def test_do_version_changes_for_db_pinned(self, mock_target_version,
mock_convert):
# obj is same version as pinned, no conversion done
mock_target_version.return_value = '1.4'
obj = MyObj(self.context)
obj.VERSION = '1.4'
self.assertEqual('1.4', obj.get_target_version())
obj.foo = 123
obj.bar = 'test'
self.assertEqual({'foo': 123, 'bar': 'test'}, obj.obj_get_changes())
self.assertEqual('1.4', obj.VERSION)
changes = obj.do_version_changes_for_db()
self.assertEqual({'foo': 123, 'bar': 'test', 'version': '1.4'},
changes)
self.assertEqual('1.4', obj.VERSION)
mock_target_version.assert_called_with()
self.assertFalse(mock_convert.called)
@mock.patch.object(base.IronicObject, 'get_target_version',
spec_set=types.FunctionType)
def test_do_version_changes_for_db_downgrade(self, mock_target_version):
# obj is 1.5; convert to 1.4
mock_target_version.return_value = '1.4'
obj = MyObj(self.context)
obj.foo = 123
obj.bar = 'test'
obj.missing = 'something'
self.assertEqual({'foo': 123, 'bar': 'test', 'missing': 'something'},
obj.obj_get_changes())
self.assertEqual('1.5', obj.VERSION)
changes = obj.do_version_changes_for_db()
self.assertEqual({'foo': 123, 'bar': 'test', 'missing': '',
'version': '1.4'}, changes)
self.assertEqual('1.4', obj.VERSION)
mock_target_version.assert_called_with()
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def _test__from_db_object(self, version, mock_release_mapping):
mock_release_mapping.__getitem__.return_value = {
'objects': {
'MyObj': ['1.4'],
}
}
missing = ''
if version == '1.5':
missing = 'foo'
obj = MyObj(self.context)
dbobj = {'created_at': timeutils.utcnow(),
'updated_at': timeutils.utcnow(),
'version': version,
'foo': 123, 'bar': 'test', 'missing': missing}
MyObj._from_db_object(self.context, obj, dbobj)
self.assertEqual(obj.__class__.VERSION, obj.VERSION)
self.assertEqual(123, obj.foo)
self.assertEqual('test', obj.bar)
self.assertEqual('foo', obj.missing)
self.assertFalse(mock_release_mapping.called)
def test__from_db_object(self):
self._test__from_db_object('1.5')
def test__from_db_object_old(self):
self._test__from_db_object('1.4')
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def test__from_db_object_no_version(self, mock_release_mapping):
# DB doesn't have version; get it from mapping
mock_release_mapping.__getitem__.return_value = {
'objects': {
'MyObj': ['1.4'],
}
}
obj = MyObj(self.context)
dbobj = {'created_at': timeutils.utcnow(),
'updated_at': timeutils.utcnow(),
'version': None,
'foo': 123, 'bar': 'test', 'missing': ''}
MyObj._from_db_object(self.context, obj, dbobj)
self.assertEqual(obj.__class__.VERSION, obj.VERSION)
self.assertEqual(123, obj.foo)
self.assertEqual('test', obj.bar)
self.assertEqual('foo', obj.missing)
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def test__from_db_object_no_version_subset_of_fields(self,
mock_release_mapping):
# DB doesn't have version; get it from mapping
mock_release_mapping.__getitem__.return_value = {
'objects': {
'MyObj': ['1.5'],
}
}
obj = MyObj(self.context)
dbobj = {'created_at': timeutils.utcnow(),
'updated_at': timeutils.utcnow(),
'version': None,
'foo': 123, 'bar': 'test', 'missing': '',
'nested_object': 'test'}
# Mock obj_load_attr as this is what is called if an attribute that we
# try to access is not yet set. For all ironic objects it's a noop,
# we've implemented it in MyObj purely for testing
with mock.patch.object(obj, 'obj_load_attr', autospec=True):
MyObj._from_db_object(self.context, obj, dbobj,
fields=['foo', 'bar'])
self.assertEqual(obj.__class__.VERSION, obj.VERSION)
self.assertEqual(123, obj.foo)
self.assertEqual('test', obj.bar)
self.assertRaises(AttributeError, getattr, obj, 'missing')
self.assertEqual('testtest', obj.nested_object)
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def test__from_db_object_map_version_bad(self, mock_release_mapping):
mock_release_mapping.__getitem__.return_value = {
'objects': {
'MyObj': ['1.6'],
}
}
obj = MyObj(self.context)
dbobj = {'created_at': timeutils.utcnow(),
'updated_at': timeutils.utcnow(),
'version': None,
'foo': 123, 'bar': 'test', 'missing': ''}
self.assertRaises(object_exception.IncompatibleObjectVersion,
MyObj._from_db_object, self.context, obj, dbobj)
def test_get_target_version_no_pin(self):
obj = MyObj(self.context)
self.assertEqual('1.5', obj.get_target_version())
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def test_get_target_version_pinned(self, mock_release_mapping):
CONF.set_override('pin_release_version',
release_mappings.RELEASE_VERSIONS[-1])
mock_release_mapping.__getitem__.return_value = {
'objects': {
'MyObj': ['1.4'],
}
}
obj = MyObj(self.context)
self.assertEqual('1.4', obj.get_target_version())
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def test_get_target_version_pinned_no_myobj(self, mock_release_mapping):
CONF.set_override('pin_release_version',
release_mappings.RELEASE_VERSIONS[-1])
mock_release_mapping.__getitem__.return_value = {
'objects': {
'NotMyObj': ['1.4'],
}
}
obj = MyObj(self.context)
self.assertEqual('1.5', obj.get_target_version())
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def test_get_target_version_pinned_bad(self, mock_release_mapping):
CONF.set_override('pin_release_version',
release_mappings.RELEASE_VERSIONS[-1])
mock_release_mapping.__getitem__.return_value = {
'objects': {
'MyObj': ['1.6'],
}
}
obj = MyObj(self.context)
self.assertRaises(object_exception.IncompatibleObjectVersion,
obj.get_target_version)
@mock.patch.object(base.IronicObject, 'get_target_version',
spec_set=types.FunctionType)
def test_supports_version(self, mock_target_version):
mock_target_version.return_value = "1.5"
obj = MyObj(self.context)
self.assertTrue(obj.supports_version((1, 5)))
self.assertFalse(obj.supports_version((1, 6)))
def test_obj_fields(self):
@base.IronicObjectRegistry.register_if(False)
class TestObj(base.IronicObject,
object_base.VersionedObjectDictCompat):
fields = {'foo': fields.IntegerField()}
obj_extra_fields = ['bar']
@property
def bar(self):
return 'this is bar'
obj = TestObj(self.context)
self.assertEqual(set(['created_at', 'updated_at', 'foo', 'bar']),
set(obj.obj_fields))
def test_refresh_object(self):
@base.IronicObjectRegistry.register_if(False)
class TestObj(base.IronicObject,
object_base.VersionedObjectDictCompat):
fields = {'foo': fields.IntegerField(),
'bar': fields.StringField()}
obj = TestObj(self.context)
current_obj = TestObj(self.context)
obj.foo = 10
obj.bar = 'obj.bar'
current_obj.foo = 2
current_obj.bar = 'current.bar'
obj.obj_refresh(current_obj)
self.assertEqual(2, obj.foo)
self.assertEqual('current.bar', obj.bar)
def test_obj_constructor(self):
obj = MyObj(self.context, foo=123, bar='abc')
self.assertEqual(123, obj.foo)
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
# The hashes are to help developers to check if a change in an object needs a
# version bump. It is an MD5 hash of the object fields and remotable methods.
# The fingerprint values should only be changed if there is a version bump.
expected_object_fingerprints = {
'Node': '1.22-f2c453dd0b42aec8d4833a69a9ac79f3',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
'Port': '1.7-898a47921f4a1f53fcdddd4eeb179e0b',
'Portgroup': '1.3-71923a81a86743b313b190f5c675e258',
'Conductor': '1.2-5091f249719d4a465062a1b3dc7f860d',
'EventType': '1.1-aa2ba1afd38553e3880c267404e8d370',
'NotificationPublisher': '1.0-51a09397d6c0687771fb5be9a999605d',
'NodePayload': '1.4-1ab0efe090ee3b2bd48d280a5acec1d4',
'NodeSetPowerStateNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeSetPowerStatePayload': '1.4-9ab5116c2c59dfb9e56dcc7ef660a431',
'NodeCorrectedPowerStateNotification':
'1.0-59acc533c11d306f149846f922739c15',
'NodeCorrectedPowerStatePayload': '1.4-d624154b8fb4e6cdda1554a8bf3f4cf5',
'NodeSetProvisionStateNotification':
'1.0-59acc533c11d306f149846f922739c15',
'NodeSetProvisionStatePayload': '1.4-605c17b779aeedf0925937b2785d9f43',
'VolumeConnector': '1.0-3e0252c0ab6e6b9d158d09238a577d97',
'VolumeTarget': '1.0-0b10d663d8dae675900b2c7548f76f5e',
'ChassisCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'ChassisCRUDPayload': '1.0-dce63895d8186279a7dd577cffccb202',
'NodeCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeCRUDPayload': '1.2-b7a265a5e2fe47adada3c7c20c68e465',
'PortCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortCRUDPayload': '1.2-233d259df442eb15cc584fae1fe81504',
'NodeMaintenanceNotification': '1.0-59acc533c11d306f149846f922739c15',
'NodeConsoleNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortgroupCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'PortgroupCRUDPayload': '1.0-b73c1fecf0cef3aa56bbe3c7e2275018',
'VolumeConnectorCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'VolumeConnectorCRUDPayload': '1.0-5e8dbb41e05b6149d8f7bfd4daff9339',
'VolumeTargetCRUDNotification': '1.0-59acc533c11d306f149846f922739c15',
'VolumeTargetCRUDPayload': '1.0-30dcc4735512c104a3a36a2ae1e2aeb2',
}
class TestObjectVersions(test_base.TestCase):
def test_object_version_check(self):
classes = base.IronicObjectRegistry.obj_classes()
checker = object_fixture.ObjectVersionChecker(obj_classes=classes)
# Compute the difference between actual fingerprints and
# expect fingerprints. expect = actual = {} if there is no change.
expect, actual = checker.test_hashes(expected_object_fingerprints)
self.assertEqual(expect, actual,
"Some objects fields or remotable methods have been "
"modified. Please make sure the version of those "
"objects have been bumped and then update "
"expected_object_fingerprints with the new hashes. ")
class TestObjectSerializer(test_base.TestCase):
def test_object_serialization(self):
ser = base.IronicObjectSerializer()
obj = MyObj(self.context)
primitive = ser.serialize_entity(self.context, obj)
self.assertIn('ironic_object.name', primitive)
obj2 = ser.deserialize_entity(self.context, primitive)
self.assertIsInstance(obj2, MyObj)
self.assertEqual(self.context, obj2._context)
def test_object_serialization_iterables(self):
ser = base.IronicObjectSerializer()
obj = MyObj(self.context)
for iterable in (list, tuple, set):
thing = iterable([obj])
primitive = ser.serialize_entity(self.context, thing)
self.assertEqual(1, len(primitive))
for item in primitive:
self.assertNotIsInstance(item, base.IronicObject)
thing2 = ser.deserialize_entity(self.context, primitive)
self.assertEqual(1, len(thing2))
for item in thing2:
self.assertIsInstance(item, MyObj)
@mock.patch('ironic.objects.base.IronicObject.indirection_api',
autospec=True)
def _test_deserialize_entity_newer(self, obj_version, backported_to,
mock_indirection_api,
my_version='1.6'):
ser = base.IronicObjectSerializer()
backported_obj = MyObj()
mock_indirection_api.object_backport_versions.return_value \
= backported_obj
@base.IronicObjectRegistry.register
class MyTestObj(MyObj):
VERSION = my_version
obj = MyTestObj(self.context)
obj.VERSION = obj_version
primitive = obj.obj_to_primitive()
result = ser.deserialize_entity(self.context, primitive)
if backported_to is None:
self.assertFalse(
mock_indirection_api.object_backport_versions.called)
else:
self.assertEqual(backported_obj, result)
versions = object_base.obj_tree_get_versions('MyTestObj')
mock_indirection_api.object_backport_versions.assert_called_with(
self.context, primitive, versions)
def test_deserialize_entity_newer_version_backports(self):
"Test object with unsupported (newer) version"
self._test_deserialize_entity_newer('1.25', '1.6')
def test_deserialize_entity_same_revision_does_not_backport(self):
"Test object with supported revision"
self._test_deserialize_entity_newer('1.6', None)
def test_deserialize_entity_newer_revision_does_not_backport_zero(self):
"Test object with supported revision"
self._test_deserialize_entity_newer('1.6.0', None)
def test_deserialize_entity_newer_revision_does_not_backport(self):
"Test object with supported (newer) revision"
self._test_deserialize_entity_newer('1.6.1', None)
def test_deserialize_entity_newer_version_passes_revision(self):
"Test object with unsupported (newer) version and revision"
self._test_deserialize_entity_newer('1.7', '1.6.1', my_version='1.6.1')
@mock.patch('ironic.common.release_mappings.RELEASE_MAPPING',
autospec=True)
def test_deserialize_entity_pin_ignored(self, mock_release_mapping):
# Deserializing doesn't look at pinning
CONF.set_override('pin_release_version',
release_mappings.RELEASE_VERSIONS[-1])
mock_release_mapping.__getitem__.return_value = {
'objects': {
'MyTestObj': ['1.0'],
}
}
ser = base.IronicObjectSerializer()
@base.IronicObjectRegistry.register
class MyTestObj(MyObj):
VERSION = '1.1'
obj = MyTestObj(self.context)
primitive = obj.obj_to_primitive()
result = ser.deserialize_entity(self.context, primitive)
self.assertEqual('1.1', result.VERSION)
self.assertEqual('1.0', result.get_target_version())
self.assertFalse(mock_release_mapping.called)
@mock.patch.object(base.IronicObject, 'convert_to_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
def test_serialize_entity_unpinned_api(self, mock_version, mock_convert):
"""Test single element serializer with no backport, unpinned."""
mock_version.return_value = MyObj.VERSION
serializer = base.IronicObjectSerializer(is_server=False)
obj = MyObj(self.context)
obj.foo = 1
obj.bar = 'text'
obj.missing = 'textt'
primitive = serializer.serialize_entity(self.context, obj)
self.assertEqual('1.5', primitive['ironic_object.version'])
data = primitive['ironic_object.data']
self.assertEqual(1, data['foo'])
self.assertEqual('text', data['bar'])
self.assertEqual('textt', data['missing'])
changes = primitive['ironic_object.changes']
self.assertEqual(set(['foo', 'bar', 'missing']), set(changes))
self.assertFalse(mock_version.called)
self.assertFalse(mock_convert.called)
@mock.patch.object(base.IronicObject, 'convert_to_version', autospec=True)
@mock.patch.object(base.IronicObject, 'get_target_version',
spec_set=types.FunctionType)
def test_serialize_entity_unpinned_conductor(self, mock_version,
mock_convert):
"""Test single element serializer with no backport, unpinned."""
mock_version.return_value = MyObj.VERSION
serializer = base.IronicObjectSerializer(is_server=True)
obj = MyObj(self.context)
obj.foo = 1
obj.bar = 'text'
obj.missing = 'textt'
primitive = serializer.serialize_entity(self.context, obj)
self.assertEqual('1.5', primitive['ironic_object.version'])
data = primitive['ironic_object.data']
self.assertEqual(1, data['foo'])
self.assertEqual('text', data['bar'])
self.assertEqual('textt', data['missing'])
changes = primitive['ironic_object.changes']
self.assertEqual(set(['foo', 'bar', 'missing']), set(changes))
mock_version.assert_called_once_with()
self.assertFalse(mock_convert.called)
@mock.patch.object(base.IronicObject, 'get_target_version', autospec=True)
def test_serialize_entity_pinned_api(self, mock_version):
"""Test single element serializer with backport to pinned version."""
mock_version.return_value = '1.4'
serializer = base.IronicObjectSerializer(is_server=False)
obj = MyObj(self.context)
obj.foo = 1
obj.bar = 'text'
obj.missing = 'miss'
self.assertEqual('1.5', obj.VERSION)
primitive = serializer.serialize_entity(self.context, obj)
self.assertEqual('1.5', primitive['ironic_object.version'])
data = primitive['ironic_object.data']
self.assertEqual(1, data['foo'])
self.assertEqual('text', data['bar'])
self.assertEqual('miss', data['missing'])
self.assertFalse(mock_version.called)
@mock.patch.object(base.IronicObject, 'get_target_version',
spec_set=types.FunctionType)
def test_serialize_entity_pinned_conductor(self, mock_version):
"""Test single element serializer with backport to pinned version."""
mock_version.return_value = '1.4'
serializer = base.IronicObjectSerializer(is_server=True)
obj = MyObj(self.context)
obj.foo = 1
obj.bar = 'text'
obj.missing = 'miss'
self.assertEqual('1.5', obj.VERSION)
primitive = serializer.serialize_entity(self.context, obj)
self.assertEqual('1.4', primitive['ironic_object.version'])
data = primitive['ironic_object.data']
self.assertEqual(1, data['foo'])
self.assertEqual('text', data['bar'])
self.assertNotIn('missing', data)
self.assertNotIn('ironic_object.changes', primitive)
mock_version.assert_called_once_with()
@mock.patch.object(base.IronicObject, 'get_target_version',
spec_set=types.FunctionType)
def test_serialize_entity_invalid_pin(self, mock_version):
mock_version.side_effect = object_exception.InvalidTargetVersion(
version='1.6')
serializer = base.IronicObjectSerializer(is_server=True)
obj = MyObj(self.context)
self.assertRaises(object_exception.InvalidTargetVersion,
serializer.serialize_entity, self.context, obj)
mock_version.assert_called_once_with()
@mock.patch.object(base.IronicObject, 'convert_to_version', autospec=True)
def _test__process_object(self, mock_convert, is_server=True):
obj = MyObj(self.context)
obj.foo = 1
obj.bar = 'text'
obj.missing = 'miss'
primitive = obj.obj_to_primitive()
serializer = base.IronicObjectSerializer(is_server=is_server)
obj2 = serializer._process_object(self.context, primitive)
self.assertEqual(obj.foo, obj2.foo)
self.assertEqual(obj.bar, obj2.bar)
self.assertEqual(obj.missing, obj2.missing)
self.assertEqual(obj.VERSION, obj2.VERSION)
self.assertFalse(mock_convert.called)
def test__process_object_api(self):
self._test__process_object(is_server=False)
def test__process_object_conductor(self):
self._test__process_object(is_server=True)
@mock.patch.object(base.IronicObject, 'convert_to_version', autospec=True)
def _test__process_object_convert(self, is_server, mock_convert):
obj = MyObj(self.context)
obj.foo = 1
obj.bar = 'text'
obj.missing = ''
obj.VERSION = '1.4'
primitive = obj.obj_to_primitive()
serializer = base.IronicObjectSerializer(is_server=is_server)
serializer._process_object(self.context, primitive)
mock_convert.assert_called_once_with(
mock.ANY, '1.5', remove_unavailable_fields=not is_server)
def test__process_object_convert_api(self):
self._test__process_object_convert(False)
def test__process_object_convert_conductor(self):
self._test__process_object_convert(True)
class TestRegistry(test_base.TestCase):
@mock.patch('ironic.objects.base.objects', autospec=True)
def test_hook_chooses_newer_properly(self, mock_objects):
reg = base.IronicObjectRegistry()
reg.registration_hook(MyObj, 0)
class MyNewerObj(object):
VERSION = '1.123'
@classmethod
def obj_name(cls):
return 'MyObj'
self.assertEqual(MyObj, mock_objects.MyObj)
reg.registration_hook(MyNewerObj, 0)
self.assertEqual(MyNewerObj, mock_objects.MyObj)
@mock.patch('ironic.objects.base.objects', autospec=True)
def test_hook_keeps_newer_properly(self, mock_objects):
reg = base.IronicObjectRegistry()
reg.registration_hook(MyObj, 0)
class MyOlderObj(object):
VERSION = '1.1'
@classmethod
def obj_name(cls):
return 'MyObj'
self.assertEqual(MyObj, mock_objects.MyObj)
reg.registration_hook(MyOlderObj, 0)
self.assertEqual(MyObj, mock_objects.MyObj)