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:
parent
76231f3ad2
commit
dd119d5620
@ -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"
|
||||
|
@ -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.
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user