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

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. 

14 

15"""Nova common internal object model""" 

16 

17import contextlib 

18import datetime 

19import functools 

20import traceback 

21 

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 

28 

29from nova import exception 

30from nova import objects 

31from nova.objects import fields as obj_fields 

32from nova import utils 

33 

34 

35LOG = logging.getLogger(__name__) 

36 

37 

38def all_things_equal(obj_a, obj_b): 

39 if obj_b is None: 

40 return False 

41 

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 

49 

50 if getattr(obj_a, name) != getattr(obj_b, name): 

51 return False 

52 return True 

53 

54 

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 

60 

61 

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)) 

69 

70 

71class NovaObjectRegistry(ovoo_base.VersionedObjectRegistry): 

72 notification_classes = [] 

73 

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) 

86 

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 

96 

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) 

105 

106 

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 

112 

113 

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) 

123 

124 

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)) 

138 

139 return wrapper 

140 

141 

142class NovaObject(ovoo_base.VersionedObject): 

143 """Base class and object factory. 

144 

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 """ 

151 

152 OBJ_SERIAL_NAMESPACE = 'nova_object' 

153 OBJ_PROJECT_NAMESPACE = 'nova' 

154 

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 

160 

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 

165 

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() 

170 

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 

180 

181 

182class NovaPersistentObject(object): 

183 """Mixin class for Persistent objects. 

184 

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 } 

193 

194 

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. 

200 

201 If an object should have all fields with a default= set to 

202 those values during instantiation, inherit from this class. 

203 

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 """ 

217 

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() 

222 

223 

224class NovaEphemeralObject(EphemeralObject, 

225 NovaObject): 

226 """Base class for objects that are not row-column in the DB. 

227 

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. 

231 

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 

236 

237 

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 

244 

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) 

253 

254 

255class NovaObjectSerializer(messaging.NoOpSerializer): 

256 """A NovaObject-aware Serializer. 

257 

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 """ 

263 

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 

270 

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 

290 

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]) 

312 

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 

321 

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 

329 

330 

331def obj_to_primitive(obj): 

332 """Recursively turn an object into a python primitive. 

333 

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 

351 

352 

353def obj_make_dict_of_lists(context, list_cls, obj_list, item_key): 

354 """Construct a dictionary of object lists, keyed by item_key. 

355 

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 """ 

361 

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 

373 

374 

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) 

404 

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)) 

410 

411 

412def obj_equal_prims(obj_1, obj_2, ignore=None): 

413 """Compare two primitives for equivalence ignoring some keys. 

414 

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. 

419 

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 """ 

426 

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 

437 

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