Convert backup_device to OVO

This commit introduces BackupDevice object to formalize data earlier
sent over RPC as an undefined dict. This is required to be able to make
non-backward compatible changes to data sent as this parameter while
maintaining compatibility with previous release - so to support rolling
upgrades.

Change-Id: Ie57d84e32ec1c5fcfac27a7bb6d4bbb189108a5b
Partial-Implements: blueprint cinder-objects
This commit is contained in:
Szymon Borkowski 2016-06-14 10:28:42 +02:00
parent 808038053f
commit a08aa7ad79
11 changed files with 267 additions and 25 deletions

View File

@ -425,17 +425,18 @@ class BackupManager(manager.SchedulerDependentManager):
backup_service = self.service.get_backup_driver(context)
properties = utils.brick_get_connector_properties()
backup_dic = self.volume_rpcapi.get_backup_device(context,
backup, volume)
try:
backup_device = backup_dic.get('backup_device')
is_snapshot = backup_dic.get('is_snapshot')
attach_info = self._attach_device(context, backup_device,
properties, is_snapshot)
backup_device = self.volume_rpcapi.get_backup_device(context,
backup,
volume)
attach_info = self._attach_device(context,
backup_device.device_obj,
properties,
backup_device.is_snapshot)
try:
device_path = attach_info['device']['path']
if isinstance(device_path, six.string_types):
if backup_dic.get('secure_enabled', False):
if backup_device.secure_enabled:
with open(device_path) as device_file:
backup_service.backup(backup, device_file)
else:
@ -448,8 +449,8 @@ class BackupManager(manager.SchedulerDependentManager):
finally:
self._detach_device(context, attach_info,
backup_device, properties,
is_snapshot)
backup_device.device_obj, properties,
backup_device.is_snapshot)
finally:
backup = objects.Backup.get_by_id(context, backup.id)
self._cleanup_temp_volumes_snapshots_when_backup_created(

View File

@ -211,3 +211,60 @@ class BackupImport(Backup):
db_backup = db.backup_create(self._context, updates)
self._from_db_object(self._context, self, db_backup)
@base.CinderObjectRegistry.register
class BackupDeviceInfo(base.CinderObject, base.CinderObjectDictCompat,
base.CinderComparableObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'volume': fields.ObjectField('Volume', nullable=True),
'snapshot': fields.ObjectField('Snapshot', nullable=True),
'secure_enabled': fields.BooleanField(default=False),
}
obj_extra_fields = ['is_snapshot', 'device_obj']
@property
def is_snapshot(self):
if self.obj_attr_is_set('snapshot') == self.obj_attr_is_set('volume'):
msg = _("Either snapshot or volume field should be set.")
raise exception.ProgrammingError(message=msg)
return self.obj_attr_is_set('snapshot')
@property
def device_obj(self):
return self.snapshot if self.is_snapshot else self.volume
# FIXME(sborkows): This should go away in early O as we stop supporting
# backward compatibility with M.
@classmethod
def from_primitive(cls, primitive, context, expected_attrs=None):
backup_device = BackupDeviceInfo()
if primitive['is_snapshot']:
if isinstance(primitive['backup_device'], objects.Snapshot):
backup_device.snapshot = primitive['backup_device']
else:
backup_device.snapshot = objects.Snapshot._from_db_object(
context, objects.Snapshot(), primitive['backup_device'],
expected_attrs=expected_attrs)
else:
if isinstance(primitive['backup_device'], objects.Volume):
backup_device.volume = primitive['backup_device']
else:
backup_device.volume = objects.Volume._from_db_object(
context, objects.Volume(), primitive['backup_device'],
expected_attrs=expected_attrs)
backup_device.secure_enabled = primitive['secure_enabled']
return backup_device
# FIXME(sborkows): This should go away in early O as we stop supporting
# backward compatibility with M.
def to_primitive(self, context):
backup_device = (db.snapshot_get(context, self.snapshot.id)
if self.is_snapshot
else db.volume_get(context, self.volume.id))
primitive = {'backup_device': backup_device,
'secure_enabled': self.secure_enabled,
'is_snapshot': self.is_snapshot}
return primitive

View File

@ -120,6 +120,7 @@ OBJ_VERSIONS.add('1.12', {'VolumeType': '1.3'})
OBJ_VERSIONS.add('1.13', {'CleanupRequest': '1.0'})
OBJ_VERSIONS.add('1.14', {'VolumeAttachmentList': '1.1'})
OBJ_VERSIONS.add('1.15', {'Volume': '1.6', 'Snapshot': '1.2'})
OBJ_VERSIONS.add('1.16', {'BackupDeviceInfo': '1.0'})
class CinderObjectRegistry(base.VersionedObjectRegistry):

View File

@ -30,7 +30,7 @@ CONF = cfg.CONF
@base.CinderObjectRegistry.register
class Snapshot(cleanable.CinderCleanableObject, base.CinderObject,
base.CinderObjectDictCompat):
base.CinderObjectDictCompat, base.CinderComparableObject):
# Version 1.0: Initial version
# Version 1.1: Changed 'status' field to use SnapshotStatusField
# Version 1.2: This object is now cleanable (adds rows to workers table)

View File

@ -587,9 +587,13 @@ class BackupTestCase(BaseBackupTest):
backup = self._create_backup_db_entry(volume_id=vol_id)
vol = objects.Volume.get_by_id(self.ctxt, vol_id)
mock_get_backup_device.return_value = {'backup_device': vol,
'secure_enabled': False,
'is_snapshot': False, }
backup_device_dict = {'backup_device': vol, 'secure_enabled': False,
'is_snapshot': False, }
mock_get_backup_device.return_value = (
objects.BackupDeviceInfo.from_primitive(backup_device_dict,
self.ctxt,
['admin_metadata',
'metadata']))
attach_info = {'device': {'path': '/dev/null'}}
mock_detach_device = self.mock_object(self.backup_mgr,
'_detach_device')
@ -635,9 +639,11 @@ class BackupTestCase(BaseBackupTest):
snap = self._create_snapshot_db_entry(volume_id = vol_id)
vol = objects.Volume.get_by_id(self.ctxt, vol_id)
mock_get_backup_device.return_value = {'backup_device': snap,
'secure_enabled': False,
'is_snapshot': True, }
mock_get_backup_device.return_value = (
objects.BackupDeviceInfo.from_primitive({
'backup_device': snap, 'secure_enabled': False,
'is_snapshot': True, },
self.ctxt, expected_attrs=['metadata']))
# TODO(walter-boring) This is to account for the missing FakeConnector
# in os-brick 1.6.0 and >

View File

@ -22,6 +22,7 @@ from cinder import exception
from cinder import objects
from cinder.objects import fields
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_snapshot
from cinder.tests.unit import fake_volume
from cinder.tests.unit import objects as test_objects
from cinder.tests.unit import utils
@ -43,6 +44,13 @@ fake_backup = {
'restore_volume_id': None,
}
vol_props = {'status': 'available', 'size': 1}
fake_vol = fake_volume.fake_db_volume(**vol_props)
snap_props = {'status': fields.BackupStatus.AVAILABLE,
'volume_id': fake_vol['id'],
'expected_attrs': ['metadata']}
fake_snap = fake_snapshot.fake_db_snapshot(**snap_props)
class TestBackup(test_objects.BaseObjectsTestCase):
@ -237,3 +245,124 @@ class TestBackupList(test_objects.BaseObjectsTestCase):
get_all_by_volume.assert_called_once_with(self.context,
fake.VOLUME_ID, None)
TestBackup._compare(self, fake_backup, backups[0])
class BackupDeviceInfoTestCase(test_objects.BaseObjectsTestCase):
def setUp(self):
super(BackupDeviceInfoTestCase, self).setUp()
self.vol_obj = fake_volume.fake_volume_obj(self.context, **vol_props)
self.snap_obj = fake_snapshot.fake_snapshot_obj(self.context,
**snap_props)
self.backup_device_dict = {'secure_enabled': False,
'is_snapshot': False, }
@mock.patch('cinder.db.volume_get', return_value=fake_vol)
def test_from_primitive_with_volume(self, mock_fake_vol):
vol_obj = self.vol_obj
self.backup_device_dict['backup_device'] = vol_obj
backup_device_info = objects.BackupDeviceInfo.from_primitive(
self.backup_device_dict, self.context)
self.assertFalse(backup_device_info.is_snapshot)
self.assertEqual(self.backup_device_dict['secure_enabled'],
backup_device_info.secure_enabled)
self.assertEqual(vol_obj, backup_device_info.volume)
self.backup_device_dict['backup_device'] = fake_vol
backup_device_info = objects.BackupDeviceInfo.from_primitive(
self.backup_device_dict, self.context)
vol_obj_from_db = objects.Volume._from_db_object(self.context,
objects.Volume(),
fake_vol)
self.assertEqual(vol_obj_from_db, backup_device_info.volume)
@mock.patch('cinder.db.snapshot_get', return_value=fake_snap)
def test_from_primitive_with_snapshot(self, mock_fake_snap):
snap_obj = self.snap_obj
self.backup_device_dict['is_snapshot'] = True
self.backup_device_dict['backup_device'] = snap_obj
backup_device_info = objects.BackupDeviceInfo.from_primitive(
self.backup_device_dict, self.context, expected_attrs=['metadata'])
self.assertTrue(backup_device_info.is_snapshot)
self.assertEqual(self.backup_device_dict['secure_enabled'],
backup_device_info.secure_enabled)
self.assertEqual(snap_obj, backup_device_info.snapshot)
self.backup_device_dict['backup_device'] = fake_snap
backup_device_info = objects.BackupDeviceInfo.from_primitive(
self.backup_device_dict, self.context, expected_attrs=['metadata'])
self.assertEqual(snap_obj, backup_device_info.snapshot)
@mock.patch('cinder.db.volume_get', return_value=fake_vol)
def test_to_primitive_with_volume(self, mock_fake_vol):
vol_obj = self.vol_obj
self.backup_device_dict['backup_device'] = fake_vol
backup_device_info = objects.BackupDeviceInfo()
backup_device_info.volume = vol_obj
backup_device_info.secure_enabled = (
self.backup_device_dict['secure_enabled'])
backup_device_ret_dict = backup_device_info.to_primitive(self.context)
self.assertEqual(self.backup_device_dict['secure_enabled'],
backup_device_ret_dict['secure_enabled'])
self.assertFalse(backup_device_ret_dict['is_snapshot'])
self.assertEqual(self.backup_device_dict['backup_device'],
backup_device_ret_dict['backup_device'])
@mock.patch('cinder.db.snapshot_get', return_value=fake_snap)
def test_to_primitive_with_snapshot(self, mock_fake_snap):
snap_obj = self.snap_obj
backup_device_info = objects.BackupDeviceInfo()
backup_device_info.snapshot = snap_obj
backup_device_info.secure_enabled = (
self.backup_device_dict['secure_enabled'])
backup_device_ret_dict = backup_device_info.to_primitive(self.context)
self.assertEqual(self.backup_device_dict['secure_enabled'],
backup_device_ret_dict['secure_enabled'])
self.assertTrue(backup_device_ret_dict['is_snapshot'])
# NOTE(sborkows): since volume in sqlalchemy snapshot is a sqlalchemy
# object too, to compare snapshots we need to convert their volumes to
# dicts.
snap_actual_dict = fake_snap
snap_ref_dict = backup_device_ret_dict['backup_device']
snap_actual_dict['volume'] = self.vol_obj.obj_to_primitive()
snap_ref_dict['volume'] = snap_ref_dict['volume']
self.assertEqual(snap_actual_dict, snap_ref_dict)
def test_is_snapshot_both_volume_and_snapshot_raises_error(self):
snap = self.snap_obj
vol = self.vol_obj
backup_device_info = objects.BackupDeviceInfo()
backup_device_info.snapshot = snap
backup_device_info.volume = vol
backup_device_info.secure_enabled = (
self.backup_device_dict['secure_enabled'])
self.assertRaises(exception.ProgrammingError, getattr,
backup_device_info, 'is_snapshot')
def test_is_snapshot_neither_volume_nor_snapshot_raises_error(self):
backup_device_info = objects.BackupDeviceInfo()
backup_device_info.secure_enabled = (
self.backup_device_dict['secure_enabled'])
self.assertRaises(exception.ProgrammingError, getattr,
backup_device_info, 'is_snapshot')
def test_device_obj_with_volume(self):
vol = self.vol_obj
backup_device_info = objects.BackupDeviceInfo()
backup_device_info.volume = vol
backup_device_info.secure_enabled = (
self.backup_device_dict['secure_enabled'])
backup_device_obj = backup_device_info.device_obj
self.assertIsInstance(backup_device_obj, objects.Volume)
self.assertEqual(vol, backup_device_obj)
def test_device_obj_with_snapshot(self):
snap = self.snap_obj
backup_device_info = objects.BackupDeviceInfo()
backup_device_info.snapshot = snap
backup_device_info.secure_enabled = (
self.backup_device_dict['secure_enabled'])
backup_device_obj = backup_device_info.device_obj
self.assertIsInstance(backup_device_obj, objects.Snapshot)
self.assertEqual(snap, backup_device_obj)

View File

@ -24,6 +24,7 @@ from cinder import test
# corresponding version bump in the affected objects.
object_data = {
'Backup': '1.4-c50f7a68bb4c400dd53dd219685b3992',
'BackupDeviceInfo': '1.0-74b3950676c690538f4bc6796bd0042e',
'BackupImport': '1.4-c50f7a68bb4c400dd53dd219685b3992',
'BackupList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'CleanupRequest': '1.0-e7c688b893e1d5537ccf65cc3eb10a28',

View File

@ -4479,11 +4479,29 @@ class VolumeTestCase(base.BaseVolumeTestCase):
mock_get_backup.assert_called_once_with(self.context, backup)
mock_secure.assert_called_once_with()
expected_result = {'backup_device': vol,
'secure_enabled': False,
expected_result = {'backup_device': vol, 'secure_enabled': False,
'is_snapshot': False}
self.assertEqual(expected_result, result)
@mock.patch.object(driver.BaseVD, 'get_backup_device')
@mock.patch.object(driver.BaseVD, 'secure_file_operations_enabled')
def test_get_backup_device_want_objects(self, mock_secure,
mock_get_backup):
vol = tests_utils.create_volume(self.context)
backup = tests_utils.create_backup(self.context, vol['id'])
mock_secure.return_value = False
mock_get_backup.return_value = (vol, False)
result = self.volume.get_backup_device(self.context,
backup, want_objects=True)
mock_get_backup.assert_called_once_with(self.context, backup)
mock_secure.assert_called_once_with()
expected_result = objects.BackupDeviceInfo.from_primitive(
{'backup_device': vol, 'secure_enabled': False,
'is_snapshot': False},
self.context)
self.assertEqual(expected_result, result)
def test_backup_use_temp_snapshot_config(self):
local_conf = self.volume.driver.configuration.local_conf
self.assertFalse(local_conf.backup_use_temp_snapshot)

View File

@ -16,6 +16,7 @@
Unit Tests for cinder.volume.rpcapi
"""
import copy
import mock
from oslo_config import cfg
from oslo_serialization import jsonutils
@ -206,6 +207,7 @@ class VolumeRpcAPITestCase(test.TestCase):
def _fake_rpc_method(*args, **kwargs):
self.fake_args = args
kwargs.pop('want_objects', None)
self.fake_kwargs = kwargs
if expected_retval is not None:
return expected_retval
@ -567,7 +569,21 @@ class VolumeRpcAPITestCase(test.TestCase):
volume=self.fake_volume,
version='3.0')
def test_get_backup_device(self):
@mock.patch('oslo_messaging.RPCClient.can_send_version',
return_value=True)
def test_get_backup_device(self, mock_can_send_version):
self._test_volume_api('get_backup_device',
rpc_method='call',
backup=self.fake_backup_obj,
volume=self.fake_volume_obj,
version='3.2')
@mock.patch('oslo_messaging.RPCClient.can_send_version',
return_value=False)
@mock.patch('cinder.objects.backup.BackupDeviceInfo.from_primitive',
return_value={})
def test_get_backup_device_old(self, mock_from_primitive,
mock_can_send_version):
self._test_volume_api('get_backup_device',
rpc_method='call',
backup=self.fake_backup_obj,

View File

@ -4192,14 +4192,18 @@ class VolumeManager(manager.CleanableManager,
LOG.debug("Obtained capabilities list: %s.", capabilities)
return capabilities
def get_backup_device(self, ctxt, backup):
def get_backup_device(self, ctxt, backup, want_objects=False):
(backup_device, is_snapshot) = (
self.driver.get_backup_device(ctxt, backup))
secure_enabled = self.driver.secure_file_operations_enabled()
backup_device_dict = {'backup_device': backup_device,
'secure_enabled': secure_enabled,
'is_snapshot': is_snapshot, }
return backup_device_dict
# TODO(sborkows): from_primitive method will be removed in O, so there
# is a need to clean here then.
return (objects.BackupDeviceInfo.from_primitive(backup_device_dict,
ctxt)
if want_objects else backup_device_dict)
def secure_file_operations_enabled(self, ctxt, volume):
secure_enabled = self.driver.secure_file_operations_enabled()

View File

@ -14,6 +14,7 @@
from cinder.common import constants
from cinder import objects
from cinder import quota
from cinder import rpc
from cinder.volume import utils
@ -110,9 +111,11 @@ class VolumeAPI(rpc.RPCAPI):
3.1 - Remove promote_replica and reenable_replication. This is
non-backward compatible, but the user-facing API was removed
back in Mitaka when introducing cheesecake replication.
3.2 - Adds support for sending objects over RPC in
get_backup_device().
"""
RPC_API_VERSION = '3.1'
RPC_API_VERSION = '3.2'
RPC_DEFAULT_VERSION = '3.0'
TOPIC = constants.VOLUME_TOPIC
BINARY = 'cinder-volume'
@ -293,9 +296,15 @@ class VolumeAPI(rpc.RPCAPI):
return cctxt.call(ctxt, 'get_capabilities', discover=discover)
def get_backup_device(self, ctxt, backup, volume):
cctxt = self._get_cctxt(volume.host)
backup_dict = cctxt.call(ctxt, 'get_backup_device', backup=backup)
return backup_dict
cctxt = self._get_cctxt(volume.host, ('3.2', '3.0'))
if cctxt.can_send_version('3.2'):
backup_obj = cctxt.call(ctxt, 'get_backup_device', backup=backup,
want_objects=True)
else:
backup_dict = cctxt.call(ctxt, 'get_backup_device', backup=backup)
backup_obj = objects.BackupDeviceInfo.from_primitive(backup_dict,
ctxt)
return backup_obj
def secure_file_operations_enabled(self, ctxt, volume):
cctxt = self._get_cctxt(volume.host)