/
base.py
493 lines (406 loc) · 20.2 KB
/
base.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
# Copyright 2015 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.
"""Cinder common internal object model"""
import contextlib
import datetime
from oslo_log import log as logging
from oslo_utils import versionutils
from oslo_versionedobjects import base
from oslo_versionedobjects import fields
from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder import objects
LOG = logging.getLogger('object')
obj_make_list = base.obj_make_list
class CinderObjectVersionsHistory(dict):
"""Helper class that maintains objects version history.
Current state of object versions is aggregated in a single version number
that explicitily identifies a set of object versions. That way a service
is able to report what objects it supports using a single string and all
the newer services will know exactly what that mean for a single object.
"""
def __init__(self):
super(CinderObjectVersionsHistory, self).__init__()
# NOTE(dulek): This is our pre-history and a starting point - Liberty.
# We want Mitaka to be able to talk to Liberty services, so we need to
# handle backporting to these objects versions (although I don't expect
# we've made a lot of incompatible changes inside the objects).
#
# If an object doesn't exist in Liberty, RPC API compatibility layer
# shouldn't send it or convert it to a dictionary.
#
# Please note that we do not need to add similar entires for each
# release. Liberty is here just for historical reasons.
self.versions = ['liberty']
self['liberty'] = {
'Backup': '1.1',
'BackupImport': '1.1',
'BackupList': '1.0',
'ConsistencyGroup': '1.1',
'ConsistencyGroupList': '1.0',
'Service': '1.0',
'ServiceList': '1.0',
'Snapshot': '1.0',
'SnapshotList': '1.0',
'Volume': '1.1',
'VolumeAttachment': '1.0',
'VolumeAttachmentList': '1.0',
'VolumeList': '1.1',
'VolumeType': '1.0',
'VolumeTypeList': '1.0',
}
def get_current(self):
return self.versions[-1]
def get_current_versions(self):
return self[self.get_current()]
def add(self, ver, updates):
self[ver] = self[self.get_current()].copy()
self.versions.append(ver)
self[ver].update(updates)
OBJ_VERSIONS = CinderObjectVersionsHistory()
# NOTE(dulek): You should add a new version here each time you bump a version
# of any object. As a second parameter you need to specify only what changed.
#
# When dropping backward compatibility with an OpenStack release we can rework
# this and remove some history while keeping the versions order.
OBJ_VERSIONS.add('1.0', {'Backup': '1.3', 'BackupImport': '1.3',
'CGSnapshot': '1.0', 'CGSnapshotList': '1.0',
'ConsistencyGroup': '1.2',
'ConsistencyGroupList': '1.1', 'Service': '1.1',
'Volume': '1.3', 'VolumeTypeList': '1.1'})
OBJ_VERSIONS.add('1.1', {'Service': '1.2', 'ServiceList': '1.1'})
OBJ_VERSIONS.add('1.2', {'Backup': '1.4', 'BackupImport': '1.4'})
OBJ_VERSIONS.add('1.3', {'Service': '1.3'})
OBJ_VERSIONS.add('1.4', {'Snapshot': '1.1'})
OBJ_VERSIONS.add('1.5', {'VolumeType': '1.1'})
class CinderObjectRegistry(base.VersionedObjectRegistry):
def registration_hook(self, cls, index):
"""Hook called when registering a class.
This method takes care of adding the class to cinder.objects namespace.
Should registering class have a method called cinder_ovo_cls_init it
will be called to support class initialization. This is convenient
for all persistent classes that need to register their models.
"""
setattr(objects, cls.obj_name(), cls)
# If registering class has a callable initialization method, call it.
if callable(getattr(cls, 'cinder_ovo_cls_init', None)):
cls.cinder_ovo_cls_init()
class CinderObject(base.VersionedObject):
# NOTE(thangp): OBJ_PROJECT_NAMESPACE needs to be set so that nova,
# cinder, and other objects can exist on the same bus and be distinguished
# from one another.
OBJ_PROJECT_NAMESPACE = 'cinder'
# NOTE(thangp): As more objects are added to cinder, each object should
# have a custom map of version compatibility. This just anchors the base
# version compatibility.
VERSION_COMPATIBILITY = {'7.0.0': '1.0'}
def cinder_obj_get_changes(self):
"""Returns a dict of changed fields with tz unaware datetimes.
Any timezone aware datetime field will be converted to UTC timezone
and returned as timezone unaware datetime.
This will allow us to pass these fields directly to a db update
method as they can't have timezone information.
"""
# Get dirtied/changed fields
changes = self.obj_get_changes()
# Look for datetime objects that contain timezone information
for k, v in changes.items():
if isinstance(v, datetime.datetime) and v.tzinfo:
# Remove timezone information and adjust the time according to
# the timezone information's offset.
changes[k] = v.replace(tzinfo=None) - v.utcoffset()
# Return modified dict
return changes
def obj_make_compatible(self, primitive, target_version):
_log_backport(self, target_version)
super(CinderObject, self).obj_make_compatible(primitive,
target_version)
def __contains__(self, name):
# We're using obj_extra_fields to provide aliases for some fields while
# in transition period. This override is to make these aliases pass
# "'foo' in obj" tests.
return name in self.obj_extra_fields or super(CinderObject,
self).__contains__(name)
class CinderObjectDictCompat(base.VersionedObjectDictCompat):
"""Mix-in to provide dictionary key access compat.
If an object needs to support attribute access using
dictionary items instead of object attributes, inherit
from this class. This should only be used as a temporary
measure until all callers are converted to use modern
attribute access.
NOTE(berrange) This class will eventually be deleted.
"""
def get(self, key, value=base._NotSpecifiedSentinel):
"""For backwards-compatibility with dict-based objects.
NOTE(danms): May be removed in the future.
"""
if key not in self.obj_fields:
# NOTE(jdg): There are a number of places where we rely on the
# old dictionary version and do a get(xxx, None).
# The following preserves that compatibility but in
# the future we'll remove this shim altogether so don't
# rely on it.
LOG.debug('Cinder object %(object_name)s has no '
'attribute named: %(attribute_name)s',
{'object_name': self.__class__.__name__,
'attribute_name': key})
return None
if (value != base._NotSpecifiedSentinel and
key not in self.obj_extra_fields and
not self.obj_attr_is_set(key)):
return value
else:
try:
return getattr(self, key)
except (exception.ObjectActionError, NotImplementedError):
# Exception when haven't set a value for non-lazy
# loadable attribute, but to mimic typical dict 'get'
# behavior we should still return None
return None
class CinderPersistentObject(object):
"""Mixin class for Persistent objects.
This adds the fields that we use in common for all persistent objects.
"""
Not = db.Not
Case = db.Case
fields = {
'created_at': fields.DateTimeField(nullable=True),
'updated_at': fields.DateTimeField(nullable=True),
'deleted_at': fields.DateTimeField(nullable=True),
'deleted': fields.BooleanField(default=False,
nullable=True),
}
@classmethod
def cinder_ovo_cls_init(cls):
"""This method is called on OVO registration and sets the DB model."""
# Persistent Versioned Objects Classes should have a DB model, and if
# they don't, then we have a problem and we must raise an exception on
# registration.
try:
cls.model = db.get_model_for_versioned_object(cls)
except (ImportError, AttributeError):
msg = _("Couldn't find ORM model for Persistent Versioned "
"Object %s.") % cls.obj_name()
raise exception.ProgrammingError(reason=msg)
@contextlib.contextmanager
def obj_as_admin(self):
"""Context manager to make an object call as an admin.
This temporarily modifies the context embedded in an object to
be elevated() and restores it after the call completes. Example
usage:
with obj.obj_as_admin():
obj.save()
"""
if self._context is None:
raise exception.OrphanedObjectError(method='obj_as_admin',
objtype=self.obj_name())
original_context = self._context
self._context = self._context.elevated()
try:
yield
finally:
self._context = original_context
@classmethod
def _get_expected_attrs(cls, context):
return None
@classmethod
def get_by_id(cls, context, id, *args, **kwargs):
# To get by id we need to have a model and for the model to
# have an id field
if 'id' not in cls.fields:
msg = (_('VersionedObject %s cannot retrieve object by id.') %
(cls.obj_name()))
raise NotImplementedError(msg)
model = db.get_model_for_versioned_object(cls)
orm_obj = db.get_by_id(context, model, id, *args, **kwargs)
expected_attrs = cls._get_expected_attrs(context)
kargs = {}
if expected_attrs:
kargs = {'expected_attrs': expected_attrs}
return cls._from_db_object(context, cls(context), orm_obj, **kargs)
def conditional_update(self, values, expected_values=None, filters=(),
save_all=False, session=None, reflect_changes=True,
order=None):
"""Compare-and-swap update.
A conditional object update that, unlike normal update, will SAVE the
contents of the update to the DB.
Update will only occur in the DB and the object if conditions are met.
If no expected_values are passed in we will default to make sure that
all fields have not been changed in the DB. Since we cannot know the
original value in the DB for dirty fields in the object those will be
excluded.
We have 4 different condition types we can use in expected_values:
- Equality: {'status': 'available'}
- Inequality: {'status': vol_obj.Not('deleting')}
- In range: {'status': ['available', 'error']
- Not in range: {'status': vol_obj.Not(['in-use', 'attaching'])
Method accepts additional filters, which are basically anything that
can be passed to a sqlalchemy query's filter method, for example:
.. code-block:: python
[~sql.exists().where(models.Volume.id == models.Snapshot.volume_id)]
We can select values based on conditions using Case objects in the
'values' argument. For example:
.. code-block:: python
has_snapshot_filter = sql.exists().where(
models.Snapshot.volume_id == models.Volume.id)
case_values = volume.Case([(has_snapshot_filter, 'has-snapshot')],
else_='no-snapshot')
volume.conditional_update({'status': case_values},
{'status': 'available'}))
And we can use DB fields using model class attribute for example to
store previous status in the corresponding field even though we don't
know which value is in the db from those we allowed:
.. code-block:: python
volume.conditional_update({'status': 'deleting',
'previous_status': volume.model.status},
{'status': ('available', 'error')})
:param values: Dictionary of key-values to update in the DB.
:param expected_values: Dictionary of conditions that must be met for
the update to be executed.
:param filters: Iterable with additional filters
:param save_all: Object may have changes that are not in the DB, this
will say whether we want those changes saved as well.
:param session: Session to use for the update
:param reflect_changes: If we want changes made in the database to be
reflected in the versioned object. This may
mean in some cases that we have to reload the
object from the database.
:param order: Specific order of fields in which to update the values
:returns: number of db rows that were updated, which can be used as a
boolean, since it will be 0 if we couldn't update the DB and
1 if we could, because we are using unique index id.
"""
if 'id' not in self.fields:
msg = (_('VersionedObject %s does not support conditional update.')
% (self.obj_name()))
raise NotImplementedError(msg)
# If no conditions are set we will require object in DB to be unchanged
if expected_values is None:
changes = self.obj_what_changed()
expected = {key: getattr(self, key)
for key in self.fields.keys()
if self.obj_attr_is_set(key) and key not in changes and
key not in self.OPTIONAL_FIELDS}
else:
# Set the id in expected_values to limit conditional update to only
# change this object
expected = expected_values.copy()
expected['id'] = self.id
# If we want to save any additional changes the object has besides the
# ones referred in values
if save_all:
changes = self.cinder_obj_get_changes()
changes.update(values)
values = changes
result = db.conditional_update(self._context, self.model, values,
expected, filters, order=order)
# If we were able to update the DB then we need to update this object
# as well to reflect new DB contents and clear the object's dirty flags
# for those fields.
if result and reflect_changes:
# If we have used a Case, a db field or an expression in values we
# don't know which value was used, so we need to read the object
# back from the DB
if any(isinstance(v, self.Case) or db.is_orm_value(v)
for v in values.values()):
# Read back object from DB
obj = type(self).get_by_id(self._context, self.id)
db_values = obj.obj_to_primitive()['versioned_object.data']
# Only update fields were changes were requested
values = {field: db_values[field]
for field, value in values.items()}
# NOTE(geguileo): We don't use update method because our objects
# will eventually move away from VersionedObjectDictCompat
for key, value in values.items():
setattr(self, key, value)
self.obj_reset_changes(values.keys())
return result
def refresh(self):
# To refresh we need to have a model and for the model to have an id
# field
if 'id' not in self.fields:
msg = (_('VersionedObject %s cannot retrieve object by id.') %
(self.obj_name()))
raise NotImplementedError(msg)
current = self.get_by_id(self._context, self.id)
for field in self.fields:
# Only update attributes that are already set. We do not want to
# unexpectedly trigger a lazy-load.
if self.obj_attr_is_set(field):
current_field = getattr(current, field)
if getattr(self, field) != current_field:
setattr(self, field, current_field)
self.obj_reset_changes()
@classmethod
def exists(cls, context, id_):
model = db.get_model_for_versioned_object(cls)
return db.resource_exists(context, model, id_)
class CinderComparableObject(base.ComparableVersionedObject):
def __eq__(self, obj):
if hasattr(obj, 'obj_to_primitive'):
return self.obj_to_primitive() == obj.obj_to_primitive()
return False
class ObjectListBase(base.ObjectListBase):
def obj_make_compatible(self, primitive, target_version):
_log_backport(self, target_version)
super(ObjectListBase, self).obj_make_compatible(primitive,
target_version)
class CinderObjectSerializer(base.VersionedObjectSerializer):
OBJ_BASE_CLASS = CinderObject
def __init__(self, version_cap=None):
super(CinderObjectSerializer, self).__init__()
self.version_cap = version_cap
# NOTE(geguileo): During upgrades we will use a manifest to ensure that
# all objects are properly backported. This allows us to properly
# backport child objects to the right version even if parent version
# has not been bumped.
if not version_cap or version_cap == OBJ_VERSIONS.get_current():
self.manifest = None
else:
if version_cap not in OBJ_VERSIONS:
raise exception.CappedVersionUnknown(version=version_cap)
self.manifest = OBJ_VERSIONS[version_cap]
def _get_capped_obj_version(self, obj):
objname = obj.obj_name()
version_dict = OBJ_VERSIONS.get(self.version_cap, {})
version_cap = version_dict.get(objname, None)
if version_cap:
cap_tuple = versionutils.convert_version_to_tuple(version_cap)
obj_tuple = versionutils.convert_version_to_tuple(obj.VERSION)
if cap_tuple > obj_tuple:
# NOTE(dulek): Do not set version cap to be higher than actual
# object version as we don't support "forwardporting" of
# objects. If service will receive an object that's too old it
# should handle it explicitly.
version_cap = None
return version_cap
def serialize_entity(self, context, entity):
if isinstance(entity, (tuple, list, set, dict)):
entity = self._process_iterable(context, self.serialize_entity,
entity)
elif (hasattr(entity, 'obj_to_primitive') and
callable(entity.obj_to_primitive)):
# NOTE(dulek): Backport outgoing object to the capped version.
backport_ver = self._get_capped_obj_version(entity)
entity = entity.obj_to_primitive(backport_ver, self.manifest)
return entity
def _log_backport(ovo, target_version):
"""Log backported versioned objects."""
if target_version and target_version != ovo.VERSION:
LOG.debug('Backporting %(obj_name)s from version %(src_vers)s '
'to version %(dst_vers)s',
{'obj_name': ovo.obj_name(),
'src_vers': ovo.VERSION,
'dst_vers': target_version})