Merge "Port base object from Nova."
This commit is contained in:
commit
29540932e7
|
@ -260,3 +260,15 @@ class IPMIFailure(IronicException):
|
|||
|
||||
class SSHConnectFailed(IronicException):
|
||||
message = _("Failed to establish SSH connection to host %(host)s.")
|
||||
|
||||
|
||||
class UnsupportedObjectError(IronicException):
|
||||
message = _('Unsupported object type %(objtype)s')
|
||||
|
||||
|
||||
class OrphanedObjectError(IronicException):
|
||||
message = _('Cannot call %(method)s on orphaned %(objtype)s object')
|
||||
|
||||
|
||||
class IncompatibleObjectVersion(IronicException):
|
||||
message = _('Version %(objver)s of %(objname)s is not supported')
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# 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.
|
|
@ -0,0 +1,368 @@
|
|||
# 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.
|
||||
|
||||
"""Ironic common internal object model"""
|
||||
|
||||
import collections
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.objects import utils as obj_utils
|
||||
from ironic.openstack.common import log as logging
|
||||
from ironic.openstack.common.rpc import serializer as rpc_serializer
|
||||
|
||||
|
||||
LOG = logging.getLogger('object')
|
||||
|
||||
|
||||
def get_attrname(name):
|
||||
"""Return the mangled name of the attribute's underlying storage."""
|
||||
return '_%s' % name
|
||||
|
||||
|
||||
def make_class_properties(cls):
|
||||
# NOTE(danms): Inherit IronicObject's base fields only
|
||||
cls.fields.update(IronicObject.fields)
|
||||
for name, typefn in cls.fields.iteritems():
|
||||
|
||||
def getter(self, name=name, typefn=typefn):
|
||||
attrname = get_attrname(name)
|
||||
if not hasattr(self, attrname):
|
||||
self.obj_load(name)
|
||||
return getattr(self, attrname)
|
||||
|
||||
def setter(self, value, name=name, typefn=typefn):
|
||||
self._changed_fields.add(name)
|
||||
try:
|
||||
return setattr(self, get_attrname(name), typefn(value))
|
||||
except Exception:
|
||||
attr = "%s.%s" % (self.obj_name(), name)
|
||||
LOG.exception(_('Error setting %(attr)s') %
|
||||
{'attr': attr})
|
||||
raise
|
||||
|
||||
setattr(cls, name, property(getter, setter))
|
||||
|
||||
|
||||
class IronicObjectMetaclass(type):
|
||||
"""Metaclass that allows tracking of object classes."""
|
||||
|
||||
# NOTE(danms): This is what controls whether object operations are
|
||||
# remoted. If this is not None, use it to remote things over RPC.
|
||||
indirection_api = None
|
||||
|
||||
def __init__(cls, names, bases, dict_):
|
||||
if not hasattr(cls, '_obj_classes'):
|
||||
# This will be set in the 'IronicObject' class.
|
||||
cls._obj_classes = collections.defaultdict(list)
|
||||
else:
|
||||
# Add the subclass to IronicObject._obj_classes
|
||||
make_class_properties(cls)
|
||||
cls._obj_classes[cls.obj_name()].append(cls)
|
||||
|
||||
|
||||
# These are decorators that mark an object's method as remotable.
|
||||
# If the metaclass is configured to forward object methods to an
|
||||
# indirection service, these will result in making an RPC call
|
||||
# instead of directly calling the implementation in the object. Instead,
|
||||
# the object implementation on the remote end will perform the
|
||||
# requested action and the result will be returned here.
|
||||
def remotable_classmethod(fn):
|
||||
"""Decorator for remotable classmethods."""
|
||||
def wrapper(cls, context, **kwargs):
|
||||
if IronicObject.indirection_api:
|
||||
result = IronicObject.indirection_api.object_class_action(
|
||||
context, cls.obj_name(), fn.__name__, cls.version, kwargs)
|
||||
else:
|
||||
result = fn(cls, context, **kwargs)
|
||||
if isinstance(result, IronicObject):
|
||||
result._context = context
|
||||
return result
|
||||
return classmethod(wrapper)
|
||||
|
||||
|
||||
# See comment above for remotable_classmethod()
|
||||
#
|
||||
# Note that this will use either the provided context, or the one
|
||||
# stashed in the object. If neither are present, the object is
|
||||
# "orphaned" and remotable methods cannot be called.
|
||||
def remotable(fn):
|
||||
"""Decorator for remotable object methods."""
|
||||
def wrapper(self, context=None, **kwargs):
|
||||
if context is None:
|
||||
context = self._context
|
||||
if context is None:
|
||||
raise exception.OrphanedObjectError(method=fn.__name__,
|
||||
objtype=self.obj_name())
|
||||
if IronicObject.indirection_api:
|
||||
updates, result = IronicObject.indirection_api.object_action(
|
||||
context, self, fn.__name__, kwargs)
|
||||
for key, value in updates.iteritems():
|
||||
if key in self.fields:
|
||||
self[key] = self._attr_from_primitive(key, value)
|
||||
self._changed_fields = set(updates.get('obj_what_changed', []))
|
||||
return result
|
||||
else:
|
||||
return fn(self, context, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
# Object versioning rules
|
||||
#
|
||||
# Each service has its set of objects, each with a version attached. When
|
||||
# a client attempts to call an object method, the server checks to see if
|
||||
# the version of that object matches (in a compatible way) its object
|
||||
# implementation. If so, cool, and if not, fail.
|
||||
def check_object_version(server, client):
|
||||
try:
|
||||
client_major, _client_minor = client.split('.')
|
||||
server_major, _server_minor = server.split('.')
|
||||
client_minor = int(_client_minor)
|
||||
server_minor = int(_server_minor)
|
||||
except ValueError:
|
||||
raise exception.IncompatibleObjectVersion(
|
||||
_('Invalid version string'))
|
||||
|
||||
if client_major != server_major:
|
||||
raise exception.IncompatibleObjectVersion(
|
||||
dict(client=client_major, server=server_major))
|
||||
if client_minor > server_minor:
|
||||
raise exception.IncompatibleObjectVersion(
|
||||
dict(client=client_minor, server=server_minor))
|
||||
|
||||
|
||||
class IronicObject(object):
|
||||
"""Base class and object factory.
|
||||
|
||||
This forms the base of all objects that can be remoted or instantiated
|
||||
via RPC. Simply defining a class that inherits from this base class
|
||||
will make it remotely instantiatable. Objects should implement the
|
||||
necessary "get" classmethod routines as well as "save" object methods
|
||||
as appropriate.
|
||||
"""
|
||||
__metaclass__ = IronicObjectMetaclass
|
||||
|
||||
# Version of this object (see rules above check_object_version())
|
||||
version = '1.0'
|
||||
|
||||
# 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.
|
||||
fields = {
|
||||
'created_at': obj_utils.datetime_or_none,
|
||||
'updated_at': obj_utils.datetime_or_none,
|
||||
'deleted_at': obj_utils.datetime_or_none,
|
||||
'deleted': int,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._changed_fields = set()
|
||||
self._context = None
|
||||
|
||||
@classmethod
|
||||
def obj_name(cls):
|
||||
"""Return a canonical name for this object which 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 cls._obj_classes:
|
||||
LOG.error(_('Unable to instantiate unregistered object type '
|
||||
'%(objtype)s') % dict(objtype=objname))
|
||||
raise exception.UnsupportedObjectError(objtype=objname)
|
||||
|
||||
compatible_match = None
|
||||
for objclass in cls._obj_classes[objname]:
|
||||
if objclass.version == objver:
|
||||
return objclass
|
||||
try:
|
||||
check_object_version(objclass.version, objver)
|
||||
compatible_match = objclass
|
||||
except exception.IncompatibleObjectVersion:
|
||||
pass
|
||||
|
||||
if compatible_match:
|
||||
return compatible_match
|
||||
|
||||
raise exception.IncompatibleObjectVersion(objname=objname,
|
||||
objver=objver)
|
||||
|
||||
_attr_created_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):
|
||||
"""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, primitive):
|
||||
"""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']
|
||||
objdata = primitive['ironic_object.data']
|
||||
objclass = cls.obj_class_from_name(objname, objver)
|
||||
self = objclass()
|
||||
for name in self.fields:
|
||||
if name in objdata:
|
||||
setattr(self, name,
|
||||
self._attr_from_primitive(name, objdata[name]))
|
||||
changes = primitive.get('ironic_object.changes', [])
|
||||
self._changed_fields = set([x for x in changes if x in self.fields])
|
||||
return self
|
||||
|
||||
_attr_created_at_to_primitive = obj_utils.dt_serializer('created_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):
|
||||
"""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") % locals())
|
||||
|
||||
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_what_changed(self):
|
||||
"""Returns a list 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()
|
||||
|
||||
# dictish syntactic sugar
|
||||
def iteritems(self):
|
||||
"""For backwards-compatibility with dict-based objects.
|
||||
|
||||
NOTE(danms): May be removed in the future.
|
||||
"""
|
||||
for name in self.fields:
|
||||
if hasattr(self, get_attrname(name)):
|
||||
yield name, getattr(self, name)
|
||||
|
||||
items = lambda self: list(self.iteritems())
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""For backwards-compatibility with dict-based objects.
|
||||
|
||||
NOTE(danms): May be removed in the future.
|
||||
"""
|
||||
return getattr(self, name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
"""For backwards-compatibility with dict-based objects.
|
||||
|
||||
NOTE(danms): May be removed in the future.
|
||||
"""
|
||||
setattr(self, name, value)
|
||||
|
||||
def get(self, key, value=None):
|
||||
"""For backwards-compatibility with dict-based objects.
|
||||
|
||||
NOTE(danms): May be removed in the future.
|
||||
"""
|
||||
return self[key]
|
||||
|
||||
|
||||
class IronicObjectSerializer(rpc_serializer.Serializer):
|
||||
"""A IronicObject-aware Serializer.
|
||||
|
||||
This implements the Oslo Serializer interface and provides the
|
||||
ability to serialize and deserialize IronicObject entities. Any service
|
||||
that needs to accept or return IronicObjects as arguments or result values
|
||||
should pass this to its RpcProxy and RpcDispatcher objects.
|
||||
"""
|
||||
def serialize_entity(self, context, entity):
|
||||
if (hasattr(entity, 'obj_to_primitive') and
|
||||
callable(entity.obj_to_primitive)):
|
||||
entity = entity.obj_to_primitive()
|
||||
return entity
|
||||
|
||||
def deserialize_entity(self, context, entity):
|
||||
if isinstance(entity, dict) and 'ironic_object.name' in entity:
|
||||
entity = IronicObject.obj_from_primitive(entity)
|
||||
entity._context = context
|
||||
return entity
|
|
@ -0,0 +1,71 @@
|
|||
# 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.
|
||||
|
||||
"""Utility methods for objects"""
|
||||
|
||||
import datetime
|
||||
import netaddr
|
||||
|
||||
from ironic.openstack.common import timeutils
|
||||
|
||||
|
||||
def datetime_or_none(dt):
|
||||
"""Validate a datetime or None value."""
|
||||
if dt is None or isinstance(dt, datetime.datetime):
|
||||
return dt
|
||||
raise ValueError('A datetime.datetime is required here')
|
||||
|
||||
|
||||
def int_or_none(val):
|
||||
"""Attempt to parse an integer value, or None."""
|
||||
if val is None:
|
||||
return val
|
||||
else:
|
||||
return int(val)
|
||||
|
||||
|
||||
def str_or_none(val):
|
||||
"""Attempt to stringify a value, or None."""
|
||||
if val is None:
|
||||
return val
|
||||
else:
|
||||
return str(val)
|
||||
|
||||
|
||||
def ip_or_none(version):
|
||||
"""Return a version-specific IP address validator."""
|
||||
def validator(val, version=version):
|
||||
if val is None:
|
||||
return val
|
||||
else:
|
||||
return netaddr.IPAddress(val, version=version)
|
||||
return validator
|
||||
|
||||
|
||||
def dt_serializer(name):
|
||||
"""Return a datetime serializer for a named attribute."""
|
||||
def serializer(self, name=name):
|
||||
if getattr(self, name) is not None:
|
||||
return timeutils.isotime(getattr(self, name))
|
||||
else:
|
||||
return None
|
||||
return serializer
|
||||
|
||||
|
||||
def dt_deserializer(instance, val):
|
||||
"""A deserializer method for datetime attributes."""
|
||||
if val is None:
|
||||
return None
|
||||
else:
|
||||
return timeutils.parse_isotime(val)
|
|
@ -0,0 +1,13 @@
|
|||
# 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.
|
|
@ -0,0 +1,376 @@
|
|||
# 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 gettext
|
||||
import netaddr
|
||||
|
||||
gettext.install('ironic')
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.objects import base
|
||||
from ironic.objects import utils
|
||||
from ironic.openstack.common import context
|
||||
from ironic.openstack.common import timeutils
|
||||
from ironic.tests.db import base as test_base
|
||||
|
||||
|
||||
class MyObj(base.IronicObject):
|
||||
version = '1.5'
|
||||
fields = {'foo': int,
|
||||
'bar': str,
|
||||
'missing': str,
|
||||
}
|
||||
|
||||
def obj_load(self, attrname):
|
||||
setattr(self, attrname, 'loaded!')
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get(cls, context):
|
||||
obj = cls()
|
||||
obj.foo = 1
|
||||
obj.bar = 'bar'
|
||||
obj.obj_reset_changes()
|
||||
return obj
|
||||
|
||||
@base.remotable
|
||||
def marco(self, context):
|
||||
return 'polo'
|
||||
|
||||
@base.remotable
|
||||
def update_test(self, context):
|
||||
if context.tenant == 'alternate':
|
||||
self.bar = 'alternate-context'
|
||||
else:
|
||||
self.bar = 'updated'
|
||||
|
||||
@base.remotable
|
||||
def save(self, context):
|
||||
self.obj_reset_changes()
|
||||
|
||||
@base.remotable
|
||||
def refresh(self, context):
|
||||
self.foo = 321
|
||||
self.bar = 'refreshed'
|
||||
self.obj_reset_changes()
|
||||
|
||||
@base.remotable
|
||||
def modify_save_modify(self, context):
|
||||
self.bar = 'meow'
|
||||
self.save()
|
||||
self.foo = 42
|
||||
|
||||
|
||||
class MyObj2(object):
|
||||
@classmethod
|
||||
def obj_name(cls):
|
||||
return 'MyObj'
|
||||
|
||||
@base.remotable_classmethod
|
||||
def get(cls, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestMetaclass(test_base.DbTestCase):
|
||||
def test_obj_tracking(self):
|
||||
|
||||
class NewBaseClass(object):
|
||||
__metaclass__ = base.IronicObjectMetaclass
|
||||
fields = {}
|
||||
|
||||
@classmethod
|
||||
def obj_name(cls):
|
||||
return cls.__name__
|
||||
|
||||
class Test1(NewBaseClass):
|
||||
@staticmethod
|
||||
def obj_name():
|
||||
return 'fake1'
|
||||
|
||||
class Test2(NewBaseClass):
|
||||
pass
|
||||
|
||||
class Test2v2(NewBaseClass):
|
||||
@staticmethod
|
||||
def obj_name():
|
||||
return 'Test2'
|
||||
|
||||
expected = {'fake1': [Test1], 'Test2': [Test2, Test2v2]}
|
||||
|
||||
self.assertEqual(expected, NewBaseClass._obj_classes)
|
||||
# The following should work, also.
|
||||
self.assertEqual(expected, Test1._obj_classes)
|
||||
self.assertEqual(expected, Test2._obj_classes)
|
||||
|
||||
|
||||
class TestUtils(test_base.DbTestCase):
|
||||
def test_datetime_or_none(self):
|
||||
dt = datetime.datetime.now()
|
||||
self.assertEqual(utils.datetime_or_none(dt), dt)
|
||||
self.assertEqual(utils.datetime_or_none(None), None)
|
||||
self.assertRaises(ValueError, utils.datetime_or_none, 'foo')
|
||||
|
||||
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(None), None)
|
||||
self.assertRaises(ValueError, utils.int_or_none, 'foo')
|
||||
|
||||
def test_str_or_none(self):
|
||||
class Obj(object):
|
||||
pass
|
||||
self.assertEqual(utils.str_or_none('foo'), 'foo')
|
||||
self.assertEqual(utils.str_or_none(1), '1')
|
||||
self.assertEqual(utils.str_or_none(None), None)
|
||||
|
||||
def test_ip_or_none(self):
|
||||
ip4 = netaddr.IPAddress('1.2.3.4', 4)
|
||||
ip6 = netaddr.IPAddress('1::2', 6)
|
||||
self.assertEqual(utils.ip_or_none(4)('1.2.3.4'), ip4)
|
||||
self.assertEqual(utils.ip_or_none(6)('1::2'), ip6)
|
||||
self.assertEqual(utils.ip_or_none(4)(None), None)
|
||||
self.assertEqual(utils.ip_or_none(6)(None), None)
|
||||
self.assertRaises(netaddr.AddrFormatError, utils.ip_or_none(4), 'foo')
|
||||
self.assertRaises(netaddr.AddrFormatError, utils.ip_or_none(6), 'foo')
|
||||
|
||||
def test_dt_serializer(self):
|
||||
class Obj(object):
|
||||
foo = utils.dt_serializer('bar')
|
||||
|
||||
obj = Obj()
|
||||
obj.bar = timeutils.parse_isotime('1955-11-05T00:00:00Z')
|
||||
self.assertEqual(obj.foo(), '1955-11-05T00:00:00Z')
|
||||
obj.bar = None
|
||||
self.assertEqual(obj.foo(), None)
|
||||
obj.bar = 'foo'
|
||||
self.assertRaises(AttributeError, obj.foo)
|
||||
|
||||
def test_dt_deserializer(self):
|
||||
dt = timeutils.parse_isotime('1955-11-05T00:00:00Z')
|
||||
self.assertEqual(utils.dt_deserializer(None, timeutils.isotime(dt)),
|
||||
dt)
|
||||
self.assertEqual(utils.dt_deserializer(None, None), None)
|
||||
self.assertRaises(ValueError, utils.dt_deserializer, None, 'foo')
|
||||
|
||||
|
||||
class _BaseTestCase(test_base.DbTestCase):
|
||||
def setUp(self):
|
||||
super(_BaseTestCase, self).setUp()
|
||||
self.remote_object_calls = list()
|
||||
|
||||
|
||||
class _LocalTest(_BaseTestCase):
|
||||
def setUp(self):
|
||||
super(_LocalTest, self).setUp()
|
||||
# Just in case
|
||||
base.IronicObject.indirection_api = None
|
||||
|
||||
def assertRemotes(self):
|
||||
self.assertEqual(self.remote_object_calls, [])
|
||||
|
||||
|
||||
@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(obj.foo, 1)
|
||||
|
||||
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(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()
|
||||
obj.foo = 1
|
||||
obj.obj_reset_changes()
|
||||
self.assertEqual(obj.obj_to_primitive(), expected)
|
||||
|
||||
def test_object_property(self):
|
||||
obj = MyObj()
|
||||
obj.foo = 1
|
||||
self.assertEqual(obj.foo, 1)
|
||||
|
||||
def test_object_property_type_error(self):
|
||||
obj = MyObj()
|
||||
|
||||
def fail():
|
||||
obj.foo = 'a'
|
||||
self.assertRaises(ValueError, fail)
|
||||
|
||||
def test_object_dict_syntax(self):
|
||||
obj = MyObj()
|
||||
obj.foo = 123
|
||||
obj.bar = 'bar'
|
||||
self.assertEqual(obj['foo'], 123)
|
||||
self.assertEqual(sorted(obj.items(), key=lambda x: x[0]),
|
||||
[('bar', 'bar'), ('foo', 123)])
|
||||
self.assertEqual(sorted(list(obj.iteritems()), key=lambda x: x[0]),
|
||||
[('bar', 'bar'), ('foo', 123)])
|
||||
|
||||
def test_load(self):
|
||||
obj = MyObj()
|
||||
self.assertEqual(obj.bar, 'loaded!')
|
||||
|
||||
def test_loaded_in_primitive(self):
|
||||
obj = MyObj()
|
||||
obj.foo = 1
|
||||
obj.obj_reset_changes()
|
||||
self.assertEqual(obj.bar, 'loaded!')
|
||||
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(obj.obj_to_primitive(), expected)
|
||||
|
||||
def test_changes_in_primitive(self):
|
||||
obj = MyObj()
|
||||
obj.foo = 123
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
primitive = obj.obj_to_primitive()
|
||||
self.assertTrue('ironic_object.changes' in primitive)
|
||||
obj2 = MyObj.obj_from_primitive(primitive)
|
||||
self.assertEqual(obj2.obj_what_changed(), set(['foo']))
|
||||
obj2.obj_reset_changes()
|
||||
self.assertEqual(obj2.obj_what_changed(), set())
|
||||
|
||||
def test_unknown_objtype(self):
|
||||
self.assertRaises(exception.UnsupportedObjectError,
|
||||
base.IronicObject.obj_class_from_name, 'foo', '1.0')
|
||||
|
||||
def test_with_alternate_context(self):
|
||||
ctxt1 = context.RequestContext('foo', 'foo')
|
||||
ctxt2 = context.RequestContext('bar', tenant='alternate')
|
||||
obj = MyObj.get(ctxt1)
|
||||
obj.update_test(ctxt2)
|
||||
self.assertEqual(obj.bar, 'alternate-context')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_orphaned_object(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj._context = None
|
||||
self.assertRaises(exception.OrphanedObjectError,
|
||||
obj.update_test)
|
||||
self.assertRemotes()
|
||||
|
||||
def test_changed_1(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj.foo = 123
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
obj.update_test(ctxt)
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo', 'bar']))
|
||||
self.assertEqual(obj.foo, 123)
|
||||
self.assertRemotes()
|
||||
|
||||
def test_changed_2(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj.foo = 123
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
obj.save(ctxt)
|
||||
self.assertEqual(obj.obj_what_changed(), set([]))
|
||||
self.assertEqual(obj.foo, 123)
|
||||
self.assertRemotes()
|
||||
|
||||
def test_changed_3(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj.foo = 123
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
obj.refresh(ctxt)
|
||||
self.assertEqual(obj.obj_what_changed(), set([]))
|
||||
self.assertEqual(obj.foo, 321)
|
||||
self.assertEqual(obj.bar, 'refreshed')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_changed_4(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
obj.bar = 'something'
|
||||
self.assertEqual(obj.obj_what_changed(), set(['bar']))
|
||||
obj.modify_save_modify(ctxt)
|
||||
self.assertEqual(obj.obj_what_changed(), set(['foo']))
|
||||
self.assertEqual(obj.foo, 42)
|
||||
self.assertEqual(obj.bar, 'meow')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_static_result(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
self.assertEqual(obj.bar, 'bar')
|
||||
result = obj.marco()
|
||||
self.assertEqual(result, 'polo')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_updates(self):
|
||||
ctxt = context.get_admin_context()
|
||||
obj = MyObj.get(ctxt)
|
||||
self.assertEqual(obj.foo, 1)
|
||||
obj.update_test()
|
||||
self.assertEqual(obj.bar, 'updated')
|
||||
self.assertRemotes()
|
||||
|
||||
def test_base_attributes(self):
|
||||
dt = datetime.datetime(1955, 11, 5)
|
||||
obj = MyObj()
|
||||
obj.created_at = dt
|
||||
obj.updated_at = dt
|
||||
obj.deleted_at = None
|
||||
expected = {'ironic_object.name': 'MyObj',
|
||||
'ironic_object.namespace': 'ironic',
|
||||
'ironic_object.version': '1.5',
|
||||
'ironic_object.changes':
|
||||
['created_at', 'deleted_at', 'updated_at'],
|
||||
'ironic_object.data':
|
||||
{'created_at': timeutils.isotime(dt),
|
||||
'updated_at': timeutils.isotime(dt),
|
||||
'deleted_at': None}
|
||||
}
|
||||
self.assertEqual(obj.obj_to_primitive(), expected)
|
||||
|
||||
|
||||
class TestObject(_LocalTest, _TestObject):
|
||||
pass
|
Loading…
Reference in New Issue