diff --git a/cinder/tests/unit/volume/drivers/test_rbd.py b/cinder/tests/unit/volume/drivers/test_rbd.py index b7568b11fac..1a1bfc64042 100644 --- a/cinder/tests/unit/volume/drivers/test_rbd.py +++ b/cinder/tests/unit/volume/drivers/test_rbd.py @@ -1,4 +1,3 @@ - # Copyright 2012 Josh Durgin # Copyright 2013 Canonical Ltd. # All Rights Reserved. @@ -1131,6 +1130,10 @@ class RBDTestCase(test.TestCase): mock.sentinel.total_capacity_gb) usage_mock.return_value = mock.sentinel.provisioned_capacity_gb + expected_fsid = 'abc' + expected_location_info = ('nondefault:%s:%s:%s:rbd' % + (self.cfg.rbd_ceph_conf, expected_fsid, + self.cfg.rbd_user)) expected = dict( volume_backend_name='RBD', replication_enabled=replication_enabled, @@ -1143,7 +1146,8 @@ class RBDTestCase(test.TestCase): thin_provisioning_support=True, provisioned_capacity_gb=mock.sentinel.provisioned_capacity_gb, max_over_subscription_ratio=1.0, - multiattach=False) + multiattach=False, + location_info=expected_location_info) if replication_enabled: targets = [{'backend_id': 'secondary-backend'}, @@ -1157,8 +1161,10 @@ class RBDTestCase(test.TestCase): self.mock_object(self.driver.configuration, 'safe_get', mock_driver_configuration) - actual = self.driver.get_volume_stats(True) - self.assertDictEqual(expected, actual) + with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid: + mock_get_fsid.return_value = expected_fsid + actual = self.driver.get_volume_stats(True) + self.assertDictEqual(expected, actual) @common_mocks @mock.patch('cinder.volume.drivers.rbd.RBDDriver._get_usage_info') @@ -1167,6 +1173,10 @@ class RBDTestCase(test.TestCase): self.mock_object(self.driver.configuration, 'safe_get', mock_driver_configuration) + expected_fsid = 'abc' + expected_location_info = ('nondefault:%s:%s:%s:rbd' % + (self.cfg.rbd_ceph_conf, expected_fsid, + self.cfg.rbd_user)) expected = dict(volume_backend_name='RBD', replication_enabled=False, vendor_name='Open Source', @@ -1178,10 +1188,13 @@ class RBDTestCase(test.TestCase): multiattach=False, provisioned_capacity_gb=0, max_over_subscription_ratio=1.0, - thin_provisioning_support=True) + thin_provisioning_support=True, + location_info=expected_location_info) - actual = self.driver.get_volume_stats(True) - self.assertDictEqual(expected, actual) + with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid: + mock_get_fsid.return_value = expected_fsid + actual = self.driver.get_volume_stats(True) + self.assertDictEqual(expected, actual) @ddt.data( # Normal case, no quota and dynamic total @@ -1925,6 +1938,91 @@ class RBDTestCase(test.TestCase): self.assertEqual(3.00, total_provision) + def test_migrate_volume_bad_volume_status(self): + self.volume_a.status = 'in-use' + ret = self.driver.migrate_volume(context, self.volume_a, None) + self.assertEqual((False, None), ret) + + def test_migrate_volume_bad_host(self): + host = { + 'capabilities': { + 'storage_protocol': 'not-ceph'}} + ret = self.driver.migrate_volume(context, self.volume_a, host) + self.assertEqual((False, None), ret) + + def test_migrate_volume_missing_location_info(self): + host = { + 'capabilities': { + 'storage_protocol': 'ceph'}} + ret = self.driver.migrate_volume(context, self.volume_a, host) + self.assertEqual((False, None), ret) + + def test_migrate_volume_invalid_location_info(self): + host = { + 'capabilities': { + 'storage_protocol': 'ceph', + 'location_info': 'foo:bar:baz'}} + ret = self.driver.migrate_volume(context, self.volume_a, host) + self.assertEqual((False, None), ret) + + @mock.patch('os_brick.initiator.linuxrbd.rbd') + @mock.patch('os_brick.initiator.linuxrbd.RBDClient') + def test_migrate_volume_mismatch_fsid(self, mock_client, mock_rbd): + host = { + 'capabilities': { + 'storage_protocol': 'ceph', + 'location_info': 'nondefault:None:abc:None:rbd'}} + + mock_client().__enter__().client.get_fsid.return_value = 'abc' + + with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid: + mock_get_fsid.return_value = 'not-abc' + ret = self.driver.migrate_volume(context, self.volume_a, host) + self.assertEqual((False, None), ret) + + mock_client().__enter__().client.get_fsid.return_value = 'not-abc' + + with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid: + mock_get_fsid.return_value = 'abc' + ret = self.driver.migrate_volume(context, self.volume_a, host) + self.assertEqual((False, None), ret) + + host = { + 'capabilities': { + 'storage_protocol': 'ceph', + 'location_info': 'nondefault:None:not-abc:None:rbd'}} + + mock_client().__enter__().client.get_fsid.return_value = 'abc' + + with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid: + mock_get_fsid.return_value = 'abc' + ret = self.driver.migrate_volume(context, self.volume_a, host) + self.assertEqual((False, None), ret) + + @mock.patch('os_brick.initiator.linuxrbd.rbd') + @mock.patch('os_brick.initiator.linuxrbd.RBDClient') + @mock.patch('cinder.volume.drivers.rbd.RBDVolumeProxy') + def test_migrate_volume(self, mock_proxy, mock_client, mock_rbd): + host = { + 'capabilities': { + 'storage_protocol': 'ceph', + 'location_info': 'nondefault:None:abc:None:rbd'}} + + mock_client().__enter__().client.get_fsid.return_value = 'abc' + + with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid, \ + mock.patch.object(self.driver, 'delete_volume') as mock_delete: + mock_get_fsid.return_value = 'abc' + proxy = mock_proxy.return_value + proxy.__enter__.return_value = proxy + ret = self.driver.migrate_volume(context, self.volume_a, + host) + proxy.copy.assert_called_once_with( + mock_client.return_value.__enter__.return_value.ioctx, + self.volume_a.name) + mock_delete.assert_called_once_with(self.volume_a) + self.assertEqual((True, None), ret) + class ManagedRBDTestCase(test_driver.BaseDriverTestCase): driver_name = "cinder.volume.drivers.rbd.RBDDriver" diff --git a/cinder/volume/drivers/rbd.py b/cinder/volume/drivers/rbd.py index baa0f22fc0d..8f64dcffcfc 100644 --- a/cinder/volume/drivers/rbd.py +++ b/cinder/volume/drivers/rbd.py @@ -20,8 +20,10 @@ import os import tempfile from eventlet import tpool +from os_brick.initiator import linuxrbd from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import excutils from oslo_utils import fileutils from oslo_utils import units import six @@ -36,6 +38,7 @@ from cinder import utils from cinder.volume import configuration from cinder.volume import driver + try: import rados import rbd @@ -451,6 +454,13 @@ class RBDDriver(driver.CloneableImageVD, return free_capacity, total_capacity def _update_volume_stats(self): + location_info = '%s:%s:%s:%s:%s' % ( + self.configuration.rbd_cluster_name, + self.configuration.rbd_ceph_conf, + self._get_fsid(), + self.configuration.rbd_user, + self.configuration.rbd_pool) + stats = { 'vendor_name': 'Open Source', 'driver_version': self.VERSION, @@ -463,9 +473,10 @@ class RBDDriver(driver.CloneableImageVD, 'multiattach': False, 'thin_provisioning_support': True, 'max_over_subscription_ratio': ( - self.configuration.safe_get('max_over_subscription_ratio')) - + self.configuration.safe_get('max_over_subscription_ratio')), + 'location_info': location_info, } + backend_name = self.configuration.safe_get('volume_backend_name') stats['volume_backend_name'] = backend_name or 'RBD' @@ -1416,7 +1427,71 @@ class RBDDriver(driver.CloneableImageVD, return {'_name_id': name_id, 'provider_location': provider_location} def migrate_volume(self, context, volume, host): - return (False, None) + + refuse_to_migrate = (False, None) + + if volume.status not in ('available', 'retyping', 'maintenance'): + LOG.debug('Only available volumes can be migrated using backend ' + 'assisted migration. Falling back to generic migration.') + return refuse_to_migrate + + if (host['capabilities']['storage_protocol'] != 'ceph'): + LOG.debug('Source and destination drivers need to be RBD ' + 'to use backend assisted migration. Falling back to ' + 'generic migration.') + return refuse_to_migrate + + loc_info = host['capabilities'].get('location_info') + + LOG.debug('Attempting RBD assisted volume migration. volume: %(id)s, ' + 'host: %(host)s, status=%(status)s.', + {'id': volume.id, 'host': host, 'status': volume.status}) + + if not loc_info: + LOG.debug('Could not find location_info in capabilities reported ' + 'by the destination driver. Falling back to generic ' + 'migration.') + return refuse_to_migrate + + try: + (rbd_cluster_name, rbd_ceph_conf, rbd_fsid, rbd_user, rbd_pool) = ( + utils.convert_str(loc_info).split(':')) + except ValueError: + LOG.error('Location info needed for backend enabled volume ' + 'migration not in correct format: %s. Falling back to ' + 'generic volume migration.', loc_info) + return refuse_to_migrate + + with linuxrbd.RBDClient(rbd_user, rbd_pool, conffile=rbd_ceph_conf, + rbd_cluster_name=rbd_cluster_name) as target: + if ((rbd_fsid != self._get_fsid() or + rbd_fsid != target.client.get_fsid())): + LOG.info('Migration between clusters is not supported. ' + 'Falling back to generic migration.') + return refuse_to_migrate + + with RBDVolumeProxy(self, volume.name, read_only=True) as source: + try: + source.copy(target.ioctx, volume.name) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error('Error copying rbd image %(vol)s to target ' + 'pool %(pool)s.', + {'vol': volume.name, 'pool': rbd_pool}) + self.RBDProxy().remove(target.ioctx, volume.name) + + try: + # If the source fails to delete for some reason, we want to leave + # the target volume in place in case deleting it might cause a lose + # of data. + self.delete_volume(volume) + except Exception: + reason = 'Failed to delete migration source volume %s.', volume.id + raise exception.VolumeMigrationFailed(reason=reason) + + LOG.info('Successful RBD assisted volume migration.') + + return (True, None) def manage_existing_snapshot_get_size(self, snapshot, existing_ref): """Return size of an existing image for manage_existing. diff --git a/releasenotes/notes/rbd-driver-assisted-migration-2d29788243060f77.yaml b/releasenotes/notes/rbd-driver-assisted-migration-2d29788243060f77.yaml new file mode 100644 index 00000000000..f508af0e0f3 --- /dev/null +++ b/releasenotes/notes/rbd-driver-assisted-migration-2d29788243060f77.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added driver-assisted volume migration to RBD driver. This allows a + volume to be efficiently copied by Ceph from one pool to another + within the same cluster.