Coverage for nova/objects/base.py: 99%
209 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
1# Copyright 2013 IBM Corp.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
15"""Nova common internal object model"""
17import contextlib
18import datetime
19import functools
20import traceback
22import netaddr
23from oslo_log import log as logging
24import oslo_messaging as messaging
25from oslo_utils import versionutils
26from oslo_versionedobjects import base as ovoo_base
27from oslo_versionedobjects import exception as ovoo_exc
29from nova import exception
30from nova import objects
31from nova.objects import fields as obj_fields
32from nova import utils
35LOG = logging.getLogger(__name__)
38def all_things_equal(obj_a, obj_b):
39 if obj_b is None:
40 return False
42 for name in obj_a.fields:
43 set_a = name in obj_a
44 set_b = name in obj_b
45 if set_a != set_b:
46 return False
47 elif not set_a:
48 continue
50 if getattr(obj_a, name) != getattr(obj_b, name):
51 return False
52 return True
55def get_attrname(name):
56 """Return the mangled name of the attribute's underlying storage."""
57 # FIXME(danms): This is just until we use o.vo's class properties
58 # and object base.
59 return '_obj_' + name
62def raise_on_too_new_values(version, primitive, field, new_values):
63 value = primitive.get(field, None)
64 if value in new_values:
65 raise exception.ObjectActionError(
66 action='obj_make_compatible',
67 reason='%s=%s not supported in version %s' %
68 (field, value, version))
71class NovaObjectRegistry(ovoo_base.VersionedObjectRegistry):
72 notification_classes = []
74 def registration_hook(self, cls, index):
75 # NOTE(danms): This is called when an object is registered,
76 # and is responsible for maintaining nova.objects.$OBJECT
77 # as the highest-versioned implementation of a given object.
78 version = versionutils.convert_version_to_tuple(cls.VERSION)
79 if not hasattr(objects, cls.obj_name()):
80 setattr(objects, cls.obj_name(), cls)
81 else:
82 cur_version = versionutils.convert_version_to_tuple(
83 getattr(objects, cls.obj_name()).VERSION)
84 if version >= cur_version:
85 setattr(objects, cls.obj_name(), cls)
87 @classmethod
88 def register_notification(cls, notification_cls):
89 """Register a class as notification.
90 Use only to register concrete notification or payload classes,
91 do not register base classes intended for inheritance only.
92 """
93 cls.register_if(False)(notification_cls)
94 cls.notification_classes.append(notification_cls)
95 return notification_cls
97 @classmethod
98 def register_notification_objects(cls):
99 """Register previously decorated notification as normal ovos.
100 This is not intended for production use but only for testing and
101 document generation purposes.
102 """
103 for notification_cls in cls.notification_classes:
104 cls.register(notification_cls)
107remotable_classmethod = ovoo_base.remotable_classmethod
108remotable = ovoo_base.remotable
109obj_make_list = ovoo_base.obj_make_list
110NovaObjectDictCompat = ovoo_base.VersionedObjectDictCompat
111NovaTimestampObject = ovoo_base.TimestampedObject
114def object_id(obj):
115 """Try to get a stable identifier for an object"""
116 if 'uuid' in obj:
117 ident = obj.uuid
118 elif 'id' in obj:
119 ident = obj.id
120 else:
121 ident = 'anonymous'
122 return '%s<%s>' % (obj.obj_name(), ident)
125def lazy_load_counter(fn):
126 """Increment lazy-load counter and warn if over threshold"""
127 @functools.wraps(fn)
128 def wrapper(self, attrname):
129 try:
130 return fn(self, attrname)
131 finally:
132 if self._lazy_loads is None:
133 self._lazy_loads = []
134 self._lazy_loads.append(attrname)
135 if len(self._lazy_loads) > 1:
136 LOG.debug('Object %s lazy-loaded attributes: %s',
137 object_id(self), ','.join(self._lazy_loads))
139 return wrapper
142class NovaObject(ovoo_base.VersionedObject):
143 """Base class and object factory.
145 This forms the base of all objects that can be remoted or instantiated
146 via RPC. Simply defining a class that inherits from this base class
147 will make it remotely instantiatable. Objects should implement the
148 necessary "get" classmethod routines as well as "save" object methods
149 as appropriate.
150 """
152 OBJ_SERIAL_NAMESPACE = 'nova_object'
153 OBJ_PROJECT_NAMESPACE = 'nova'
155 # Keep a running tally of how many times we've lazy-loaded on this object
156 # so we can warn if it happens too often. This is not serialized or part
157 # of the object that goes over the wire, so it is limited to a single
158 # service, which is fine for what we need.
159 _lazy_loads = None
161 # NOTE(ndipanov): This is nova-specific
162 @staticmethod
163 def should_migrate_data():
164 """A check that can be used to inhibit online migration behavior
166 This is usually used to check if all services that will be accessing
167 the db directly are ready for the new format.
168 """
169 raise NotImplementedError()
171 # NOTE(danms): This is nova-specific
172 @contextlib.contextmanager
173 def obj_alternate_context(self, context):
174 original_context = self._context
175 self._context = context
176 try:
177 yield
178 finally:
179 self._context = original_context
182class NovaPersistentObject(object):
183 """Mixin class for Persistent objects.
185 This adds the fields that we use in common for most persistent objects.
186 """
187 fields = {
188 'created_at': obj_fields.DateTimeField(nullable=True),
189 'updated_at': obj_fields.DateTimeField(nullable=True),
190 'deleted_at': obj_fields.DateTimeField(nullable=True),
191 'deleted': obj_fields.BooleanField(default=False),
192 }
195# NOTE(danms): This is copied from oslo.versionedobjects ahead of
196# a release. Do not use it directly or modify it.
197# TODO(danms): Remove this when we can get it from oslo.versionedobjects
198class EphemeralObject(object):
199 """Mix-in to provide more recognizable field defaulting.
201 If an object should have all fields with a default= set to
202 those values during instantiation, inherit from this class.
204 The base VersionedObject class is designed in such a way that all
205 fields are optional, which makes sense when representing a remote
206 database row where not all columns are transported across RPC and
207 not all columns should be set during an update operation. This is
208 why fields with default= are not set implicitly during object
209 instantiation, to avoid clobbering existing fields in the
210 database. However, objects based on VersionedObject are also used
211 to represent all-or-nothing blobs stored in the database, or even
212 used purely in RPC to represent things that are not ever stored in
213 the database. Thus, this mix-in is provided for these latter
214 object use cases where the desired behavior is to always have
215 default= fields be set at __init__ time.
216 """
218 def __init__(self, *args, **kwargs):
219 super(EphemeralObject, self).__init__(*args, **kwargs)
220 # Not specifying any fields causes all defaulted fields to be set
221 self.obj_set_defaults()
224class NovaEphemeralObject(EphemeralObject,
225 NovaObject):
226 """Base class for objects that are not row-column in the DB.
228 Objects that are used purely over RPC (i.e. not persisted) or are
229 written to the database in blob form or otherwise do not represent
230 rows directly as fields should inherit from this object.
232 The principal difference is that fields with a default value will
233 be set at __init__ time instead of requiring manual intervention.
234 """
235 pass
238class ObjectListBase(ovoo_base.ObjectListBase):
239 # NOTE(danms): These are for transition to using the oslo
240 # base object and can be removed when we move to it.
241 @classmethod
242 def _obj_primitive_key(cls, field):
243 return 'nova_object.%s' % field
245 @classmethod
246 def _obj_primitive_field(cls, primitive, field,
247 default=obj_fields.UnspecifiedDefault):
248 key = cls._obj_primitive_key(field)
249 if default == obj_fields.UnspecifiedDefault:
250 return primitive[key]
251 else:
252 return primitive.get(key, default)
255class NovaObjectSerializer(messaging.NoOpSerializer):
256 """A NovaObject-aware Serializer.
258 This implements the Oslo Serializer interface and provides the
259 ability to serialize and deserialize NovaObject entities. Any service
260 that needs to accept or return NovaObjects as arguments or result values
261 should pass this to its RPCClient and RPCServer objects.
262 """
264 @property
265 def conductor(self):
266 if not hasattr(self, '_conductor'):
267 from nova import conductor
268 self._conductor = conductor.API()
269 return self._conductor
271 def _process_object(self, context, objprim):
272 try:
273 objinst = NovaObject.obj_from_primitive(objprim, context=context)
274 except ovoo_exc.IncompatibleObjectVersion:
275 objver = objprim['nova_object.version']
276 if objver.count('.') == 2:
277 # NOTE(danms): For our purposes, the .z part of the version
278 # should be safe to accept without requiring a backport
279 objprim['nova_object.version'] = \
280 '.'.join(objver.split('.')[:2])
281 return self._process_object(context, objprim)
282 objname = objprim['nova_object.name']
283 version_manifest = ovoo_base.obj_tree_get_versions(objname)
284 if objname in version_manifest: 284 ↛ 288line 284 didn't jump to line 288 because the condition on line 284 was always true
285 objinst = self.conductor.object_backport_versions(
286 context, objprim, version_manifest)
287 else:
288 raise
289 return objinst
291 def _process_iterable(self, context, action_fn, values):
292 """Process an iterable, taking an action on each value.
293 :param:context: Request context
294 :param:action_fn: Action to take on each item in values
295 :param:values: Iterable container of things to take action on
296 :returns: A new container of the same type (except set) with
297 items from values having had action applied.
298 """
299 iterable = values.__class__
300 if issubclass(iterable, dict):
301 return iterable(**{k: action_fn(context, v)
302 for k, v in values.items()})
303 else:
304 # NOTE(danms, gibi) A set can't have an unhashable value inside,
305 # such as a dict. Convert the set to list, which is fine, since we
306 # can't send them over RPC anyway. We convert it to list as this
307 # way there will be no semantic change between the fake rpc driver
308 # used in functional test and a normal rpc driver.
309 if iterable == set:
310 iterable = list
311 return iterable([action_fn(context, value) for value in values])
313 def serialize_entity(self, context, entity):
314 if isinstance(entity, (tuple, list, set, dict)):
315 entity = self._process_iterable(context, self.serialize_entity,
316 entity)
317 elif (hasattr(entity, 'obj_to_primitive') and
318 callable(entity.obj_to_primitive)):
319 entity = entity.obj_to_primitive()
320 return entity
322 def deserialize_entity(self, context, entity):
323 if isinstance(entity, dict) and 'nova_object.name' in entity:
324 entity = self._process_object(context, entity)
325 elif isinstance(entity, (tuple, list, set, dict)):
326 entity = self._process_iterable(context, self.deserialize_entity,
327 entity)
328 return entity
331def obj_to_primitive(obj):
332 """Recursively turn an object into a python primitive.
334 A NovaObject becomes a dict, and anything that implements ObjectListBase
335 becomes a list.
336 """
337 if isinstance(obj, ObjectListBase):
338 return [obj_to_primitive(x) for x in obj]
339 elif isinstance(obj, NovaObject):
340 result = {}
341 for key in obj.obj_fields:
342 if obj.obj_attr_is_set(key) or key in obj.obj_extra_fields:
343 result[key] = obj_to_primitive(getattr(obj, key))
344 return result
345 elif isinstance(obj, netaddr.IPAddress):
346 return str(obj)
347 elif isinstance(obj, netaddr.IPNetwork):
348 return str(obj)
349 else:
350 return obj
353def obj_make_dict_of_lists(context, list_cls, obj_list, item_key):
354 """Construct a dictionary of object lists, keyed by item_key.
356 :param:context: Request context
357 :param:list_cls: The ObjectListBase class
358 :param:obj_list: The list of objects to place in the dictionary
359 :param:item_key: The object attribute name to use as a dictionary key
360 """
362 obj_lists = {}
363 for obj in obj_list:
364 key = getattr(obj, item_key)
365 if key not in obj_lists:
366 obj_lists[key] = list_cls()
367 obj_lists[key].objects = []
368 obj_lists[key].objects.append(obj)
369 for key in obj_lists:
370 obj_lists[key]._context = context
371 obj_lists[key].obj_reset_changes()
372 return obj_lists
375def serialize_args(fn):
376 """Decorator that will do the arguments serialization before remoting."""
377 def wrapper(obj, *args, **kwargs):
378 args = [utils.strtime(arg) if isinstance(arg, datetime.datetime)
379 else arg for arg in args]
380 for k, v in kwargs.items():
381 if k == 'exc_val' and v:
382 try:
383 # NOTE(danms): When we run this for a remotable method,
384 # we need to attempt to format_message() the exception to
385 # get the sanitized message, and if it's not a
386 # NovaException, fall back to just the exception class
387 # name. However, a remotable will end up calling this again
388 # on the other side of the RPC call, so we must not try
389 # to do that again, otherwise we will always end up with
390 # just str. So, only do that if exc_val is an Exception
391 # class.
392 kwargs[k] = (v.format_message() if isinstance(v, Exception)
393 else v)
394 except Exception:
395 kwargs[k] = v.__class__.__name__
396 elif k == 'exc_tb' and v and not isinstance(v, str):
397 kwargs[k] = ''.join(traceback.format_tb(v))
398 elif isinstance(v, datetime.datetime):
399 kwargs[k] = utils.strtime(v)
400 if hasattr(fn, '__call__'):
401 return fn(obj, *args, **kwargs)
402 # NOTE(danms): We wrap a descriptor, so use that protocol
403 return fn.__get__(None, obj)(*args, **kwargs)
405 # NOTE(danms): Make this discoverable
406 wrapper.remotable = getattr(fn, 'remotable', False)
407 wrapper.original_fn = fn
408 return (functools.wraps(fn)(wrapper) if hasattr(fn, '__call__')
409 else classmethod(wrapper))
412def obj_equal_prims(obj_1, obj_2, ignore=None):
413 """Compare two primitives for equivalence ignoring some keys.
415 This operation tests the primitives of two objects for equivalence.
416 Object primitives may contain a list identifying fields that have been
417 changed - this is ignored in the comparison. The ignore parameter lists
418 any other keys to be ignored.
420 :param:obj1: The first object in the comparison
421 :param:obj2: The second object in the comparison
422 :param:ignore: A list of fields to ignore
423 :returns: True if the primitives are equal ignoring changes
424 and specified fields, otherwise False.
425 """
427 def _strip(prim, keys):
428 if isinstance(prim, dict):
429 for k in keys:
430 prim.pop(k, None)
431 for v in prim.values():
432 _strip(v, keys)
433 if isinstance(prim, list):
434 for v in prim:
435 _strip(v, keys)
436 return prim
438 if ignore is not None:
439 keys = ['nova_object.changes'] + ignore
440 else:
441 keys = ['nova_object.changes']
442 prim_1 = _strip(obj_1.obj_to_primitive(), keys)
443 prim_2 = _strip(obj_2.obj_to_primitive(), keys)
444 return prim_1 == prim_2