RBD: support driver-assisted volume migration

This patch implements the driver function migration_volume() and allows
a user to efficiently migrate a volume from one pool to another as long
as both pools reside in the same cluster.

Change-Id: I84b5ff546726c9eddb46badb48b24ed1905e0aa8
Implements: blueprint ceph-volume-migrate
This commit is contained in:
Jon Bernard 2017-07-20 13:19:36 -04:00
parent 76231f3ad2
commit dd119d5620
3 changed files with 188 additions and 10 deletions

View File

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

View File

@ -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.

View File

@ -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.