enable cloning for rbd-backed ephemeral disks
Currently when using rbd as an image backend, nova downloads the glance image to local disk and then copies it again into rbd. This can be very slow for large images, and wastes bandwidth as well as disk space. When the glance image is stored in the same ceph cluster, the data is being pulled out and pushed back in unnecessarily. Instead, create a copy-on-write clone of the image. This is fast, and does not depend on the size of the image. Instead of taking minutes, booting takes seconds, and is not limited by the disk copy. Add some rbd utility functions from cinder to support cloning and let the rbd imagebackend rely on librbd instead of the rbd command line tool for checking image existence. Add an ImageHandler for rbd that does the cloning if an applicable image location is available. If no such location is available, or rbd is not configured for ephemeral disks, this handler does nothing, so enable it by default. blueprint rbd-clone-image-handler Closes-bug: 1226351 Change-Id: I9b77a50206d0eda709df8356faaeeba35d232f22 Signed-off-by: Josh Durgin <josh.durgin@inktank.com> Signed-off-by: Zhi Yan Liu <zhiyanl@cn.ibm.com>
This commit is contained in:
parent
cb8968400f
commit
c25c60f6a9
|
@ -293,12 +293,12 @@ class Qcow2TestCase(_ImageTestCase, test.NoDBTestCase):
|
|||
def test_create_image_too_small(self):
|
||||
fn = self.prepare_mocks()
|
||||
self.mox.StubOutWithMock(os.path, 'exists')
|
||||
self.mox.StubOutWithMock(imagebackend.disk, 'get_disk_size')
|
||||
self.mox.StubOutWithMock(imagebackend.Qcow2, 'get_disk_size')
|
||||
if self.OLD_STYLE_INSTANCE_PATH:
|
||||
os.path.exists(self.OLD_STYLE_INSTANCE_PATH).AndReturn(False)
|
||||
os.path.exists(self.TEMPLATE_PATH).AndReturn(True)
|
||||
imagebackend.disk.get_disk_size(self.TEMPLATE_PATH
|
||||
).AndReturn(self.SIZE)
|
||||
imagebackend.Qcow2.get_disk_size(self.TEMPLATE_PATH
|
||||
).AndReturn(self.SIZE)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
image = self.image_class(self.INSTANCE, self.NAME)
|
||||
|
@ -544,6 +544,7 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
|
|||
self.mox.VerifyAll()
|
||||
|
||||
def test_cache_base_dir_exists(self):
|
||||
fn = self.mox.CreateMockAnything()
|
||||
image = self.image_class(self.INSTANCE, self.NAME)
|
||||
|
||||
self.mox.StubOutWithMock(os.path, 'exists')
|
||||
|
@ -596,21 +597,68 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
|
|||
|
||||
rbd_utils.rbd.RBD_FEATURE_LAYERING = 1
|
||||
|
||||
self.mox.StubOutWithMock(imagebackend.disk, 'get_disk_size')
|
||||
imagebackend.disk.get_disk_size(self.TEMPLATE_PATH
|
||||
).AndReturn(self.SIZE)
|
||||
rbd_name = "%s/%s" % (self.INSTANCE['name'], self.NAME)
|
||||
image = self.image_class(self.INSTANCE, self.NAME)
|
||||
self.mox.StubOutWithMock(image, 'check_image_exists')
|
||||
image.check_image_exists().AndReturn(False)
|
||||
image.check_image_exists().AndReturn(False)
|
||||
rbd_name = "%s_%s" % (self.INSTANCE['uuid'], self.NAME)
|
||||
cmd = ('--pool', self.POOL, self.TEMPLATE_PATH,
|
||||
rbd_name, '--new-format', '--id', self.USER,
|
||||
'--conf', self.CONF)
|
||||
self.libvirt_utils.import_rbd_image(self.TEMPLATE_PATH, *cmd)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
image = self.image_class(self.INSTANCE, self.NAME)
|
||||
image.create_image(fn, self.TEMPLATE_PATH, None)
|
||||
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_create_image_resize(self):
|
||||
fn = self.mox.CreateMockAnything()
|
||||
full_size = self.SIZE * 2
|
||||
fn(max_size=full_size, target=self.TEMPLATE_PATH)
|
||||
|
||||
rbd_utils.rbd.RBD_FEATURE_LAYERING = 1
|
||||
|
||||
image = self.image_class(self.INSTANCE, self.NAME)
|
||||
self.mox.StubOutWithMock(image, 'check_image_exists')
|
||||
image.check_image_exists().AndReturn(False)
|
||||
image.check_image_exists().AndReturn(False)
|
||||
rbd_name = "%s_%s" % (self.INSTANCE['uuid'], self.NAME)
|
||||
cmd = ('--pool', self.POOL, self.TEMPLATE_PATH,
|
||||
rbd_name, '--new-format', '--id', self.USER,
|
||||
'--conf', self.CONF)
|
||||
self.libvirt_utils.import_rbd_image(self.TEMPLATE_PATH, *cmd)
|
||||
self.mox.StubOutWithMock(image, 'get_disk_size')
|
||||
image.get_disk_size(rbd_name).AndReturn(self.SIZE)
|
||||
self.mox.StubOutWithMock(image.driver, 'resize')
|
||||
image.driver.resize(rbd_name, full_size)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
image.create_image(fn, self.TEMPLATE_PATH, full_size)
|
||||
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_create_image_already_exists(self):
|
||||
rbd_utils.rbd.RBD_FEATURE_LAYERING = 1
|
||||
|
||||
image = self.image_class(self.INSTANCE, self.NAME)
|
||||
self.mox.StubOutWithMock(image, 'check_image_exists')
|
||||
image.check_image_exists().AndReturn(True)
|
||||
self.mox.StubOutWithMock(image, 'get_disk_size')
|
||||
image.get_disk_size(self.TEMPLATE_PATH).AndReturn(self.SIZE)
|
||||
image.check_image_exists().AndReturn(True)
|
||||
rbd_name = "%s_%s" % (self.INSTANCE['uuid'], self.NAME)
|
||||
image.get_disk_size(rbd_name).AndReturn(self.SIZE)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
fn = self.mox.CreateMockAnything()
|
||||
image.create_image(fn, self.TEMPLATE_PATH, self.SIZE)
|
||||
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_prealloc_image(self):
|
||||
CONF.set_override('preallocate_images', 'space')
|
||||
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
# 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.
|
||||
|
||||
import mock
|
||||
|
||||
from nova import test
|
||||
from nova.virt.libvirt import imagehandler
|
||||
|
||||
|
||||
IMAGE_ID = '155d900f-4e14-4e4c-a73d-069cbf4541e6'
|
||||
IMAGE_PATH = '/var/run/instances/_base/img'
|
||||
|
||||
|
||||
class RBDTestCase(test.NoDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(RBDTestCase, self).setUp()
|
||||
self.imagehandler = imagehandler.RBDCloneImageHandler(
|
||||
rbd=mock.Mock(),
|
||||
rados=mock.Mock())
|
||||
self.imagehandler.driver.is_cloneable = mock.Mock()
|
||||
self.imagehandler.driver.clone = mock.Mock()
|
||||
|
||||
def tearDown(self):
|
||||
super(RBDTestCase, self).tearDown()
|
||||
|
||||
def test_fetch_image_no_snapshot(self):
|
||||
url = 'rbd://old_image'
|
||||
self.imagehandler.driver.is_cloneable.return_value = False
|
||||
handled = self.imagehandler._fetch_image(None, IMAGE_ID,
|
||||
dict(disk_format='raw'),
|
||||
IMAGE_PATH,
|
||||
backend_type='rbd',
|
||||
backend_location=('a', 'b'),
|
||||
location=dict(url=url))
|
||||
self.assertFalse(handled)
|
||||
self.imagehandler.driver.is_cloneable.assert_called_once_with(url,
|
||||
mock.ANY)
|
||||
self.assertFalse(self.imagehandler.driver.clone.called)
|
||||
|
||||
def test_fetch_image_non_rbd_backend(self):
|
||||
url = 'rbd://fsid/pool/image/snap'
|
||||
self.imagehandler.driver.is_cloneable.return_value = True
|
||||
handled = self.imagehandler._fetch_image(None, IMAGE_ID,
|
||||
dict(disk_format='raw'),
|
||||
IMAGE_PATH,
|
||||
backend_type='lvm',
|
||||
backend_location='/path',
|
||||
location=dict(url=url))
|
||||
self.assertFalse(handled)
|
||||
self.assertFalse(self.imagehandler.driver.clone.called)
|
||||
|
||||
def test_fetch_image_rbd_not_cloneable(self):
|
||||
url = 'rbd://fsid/pool/image/snap'
|
||||
dest_pool = 'foo'
|
||||
dest_image = 'bar'
|
||||
self.imagehandler.driver.is_cloneable.return_value = False
|
||||
handled = self.imagehandler._fetch_image(None, IMAGE_ID,
|
||||
dict(disk_format='raw'),
|
||||
IMAGE_PATH,
|
||||
backend_type='rbd',
|
||||
backend_location=(dest_pool,
|
||||
dest_image),
|
||||
location=dict(url=url))
|
||||
self.imagehandler.driver.is_cloneable.assert_called_once_with(url,
|
||||
mock.ANY)
|
||||
self.assertFalse(handled)
|
||||
|
||||
def test_fetch_image_cloneable(self):
|
||||
url = 'rbd://fsid/pool/image/snap'
|
||||
dest_pool = 'foo'
|
||||
dest_image = 'bar'
|
||||
self.imagehandler.driver.is_cloneable.return_value = True
|
||||
handled = self.imagehandler._fetch_image(None, IMAGE_ID,
|
||||
dict(disk_format='raw'),
|
||||
IMAGE_PATH,
|
||||
backend_type='rbd',
|
||||
backend_location=(dest_pool,
|
||||
dest_image),
|
||||
location=dict(url=url))
|
||||
self.imagehandler.driver.is_cloneable.assert_called_once_with(url,
|
||||
mock.ANY)
|
||||
self.imagehandler.driver.clone.assert_called_once_with(dest_pool,
|
||||
dest_image,
|
||||
'pool',
|
||||
'image',
|
||||
'snap')
|
||||
self.assertTrue(handled)
|
||||
|
||||
def test_remove_image(self):
|
||||
url = 'rbd://fsid/pool/image/snap'
|
||||
pool = 'foo'
|
||||
image = 'bar'
|
||||
self.imagehandler.driver.remove = mock.Mock()
|
||||
self.imagehandler.driver.is_cloneable.return_value = True
|
||||
handled = self.imagehandler._remove_image(None, IMAGE_ID,
|
||||
dict(disk_format='raw'),
|
||||
IMAGE_PATH,
|
||||
backend_type='rbd',
|
||||
backend_location=(pool,
|
||||
image),
|
||||
backend_dest='baz',
|
||||
location=dict(url=url))
|
||||
self.imagehandler.driver.is_cloneable.assert_called_once_with(url,
|
||||
mock.ANY)
|
||||
self.imagehandler.driver.remove.assert_called_once_with(image)
|
||||
self.assertTrue(handled)
|
||||
|
||||
def test_move_image(self):
|
||||
url = 'rbd://fsid/pool/image/snap'
|
||||
pool = 'foo'
|
||||
image = 'bar'
|
||||
dest_image = 'baz'
|
||||
self.imagehandler.driver.rename = mock.Mock()
|
||||
self.imagehandler.driver.is_cloneable.return_value = True
|
||||
handled = self.imagehandler._move_image(None, IMAGE_ID,
|
||||
dict(disk_format='raw'),
|
||||
IMAGE_PATH, IMAGE_PATH,
|
||||
backend_type='rbd',
|
||||
backend_location=(pool, image),
|
||||
backend_dest='baz',
|
||||
location=dict(url=url))
|
||||
self.imagehandler.driver.is_cloneable.assert_called_once_with(url,
|
||||
mock.ANY)
|
||||
self.imagehandler.driver.rename.assert_called_once_with(image,
|
||||
dest_image)
|
||||
self.assertTrue(handled)
|
|
@ -13,8 +13,8 @@
|
|||
|
||||
import mock
|
||||
|
||||
from nova import exception
|
||||
from nova.openstack.common import log as logging
|
||||
from nova.openstack.common import units
|
||||
from nova import test
|
||||
from nova import utils
|
||||
from nova.virt.libvirt import rbd_utils
|
||||
|
@ -77,6 +77,65 @@ class RBDTestCase(test.NoDBTestCase):
|
|||
def tearDown(self):
|
||||
super(RBDTestCase, self).tearDown()
|
||||
|
||||
def test_good_locations(self):
|
||||
locations = ['rbd://fsid/pool/image/snap',
|
||||
'rbd://%2F/%2F/%2F/%2F', ]
|
||||
map(self.driver.parse_location, locations)
|
||||
|
||||
def test_bad_locations(self):
|
||||
locations = ['rbd://image',
|
||||
'http://path/to/somewhere/else',
|
||||
'rbd://image/extra',
|
||||
'rbd://image/',
|
||||
'rbd://fsid/pool/image/',
|
||||
'rbd://fsid/pool/image/snap/',
|
||||
'rbd://///', ]
|
||||
for loc in locations:
|
||||
self.assertRaises(exception.ImageUnacceptable,
|
||||
self.driver.parse_location,
|
||||
loc)
|
||||
self.assertFalse(
|
||||
self.driver.is_cloneable(loc, {'disk_format': 'raw'}))
|
||||
|
||||
def test_cloneable(self):
|
||||
with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid:
|
||||
mock_get_fsid.return_value = 'abc'
|
||||
location = 'rbd://abc/pool/image/snap'
|
||||
info = {'disk_format': 'raw'}
|
||||
self.assertTrue(self.driver.is_cloneable(location, info))
|
||||
self.assertTrue(mock_get_fsid.called)
|
||||
|
||||
def test_uncloneable_different_fsid(self):
|
||||
with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid:
|
||||
mock_get_fsid.return_value = 'abc'
|
||||
location = 'rbd://def/pool/image/snap'
|
||||
self.assertFalse(
|
||||
self.driver.is_cloneable(location, {'disk_format': 'raw'}))
|
||||
self.assertTrue(mock_get_fsid.called)
|
||||
|
||||
@mock.patch.object(rbd_utils, 'RBDVolumeProxy')
|
||||
def test_uncloneable_unreadable(self, mock_proxy):
|
||||
with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid:
|
||||
mock_get_fsid.return_value = 'abc'
|
||||
location = 'rbd://abc/pool/image/snap'
|
||||
|
||||
mock_proxy.side_effect = self.mock_rbd.Error
|
||||
|
||||
args = [location, {'disk_format': 'raw'}]
|
||||
self.assertFalse(self.driver.is_cloneable(*args))
|
||||
mock_proxy.assert_called_once()
|
||||
self.assertTrue(mock_get_fsid.called)
|
||||
|
||||
def test_uncloneable_bad_format(self):
|
||||
with mock.patch.object(self.driver, '_get_fsid') as mock_get_fsid:
|
||||
mock_get_fsid.return_value = 'abc'
|
||||
location = 'rbd://abc/pool/image/snap'
|
||||
formats = ['qcow2', 'vmdk', 'vdi']
|
||||
for f in formats:
|
||||
self.assertFalse(
|
||||
self.driver.is_cloneable(location, {'disk_format': f}))
|
||||
self.assertTrue(mock_get_fsid.called)
|
||||
|
||||
def test_get_mon_addrs(self):
|
||||
with mock.patch.object(utils, 'execute') as mock_execute:
|
||||
mock_execute.return_value = (CEPH_MON_DUMP, '')
|
||||
|
@ -84,6 +143,35 @@ class RBDTestCase(test.NoDBTestCase):
|
|||
ports = ['6789', '6790', '6791', '6792', '6791']
|
||||
self.assertEqual((hosts, ports), self.driver.get_mon_addrs())
|
||||
|
||||
@mock.patch.object(rbd_utils, 'RADOSClient')
|
||||
def test_clone(self, mock_client):
|
||||
src_pool = u'images'
|
||||
src_image = u'image-name'
|
||||
src_snap = u'snapshot-name'
|
||||
|
||||
client_stack = []
|
||||
|
||||
def mock__enter__(inst):
|
||||
def _inner():
|
||||
client_stack.append(inst)
|
||||
return inst
|
||||
return _inner
|
||||
|
||||
client = mock_client.return_value
|
||||
# capture both rados client used to perform the clone
|
||||
client.__enter__.side_effect = mock__enter__(client)
|
||||
|
||||
self.mock_rbd.RBD.clone = mock.Mock()
|
||||
|
||||
self.driver.clone(self.rbd_pool, self.volume_name,
|
||||
src_pool, src_image, src_snap)
|
||||
|
||||
args = [client_stack[0].ioctx, str(src_image), str(src_snap),
|
||||
client_stack[1].ioctx, str(self.volume_name)]
|
||||
kwargs = {'features': self.mock_rbd.RBD_FEATURE_LAYERING}
|
||||
self.mock_rbd.RBD.clone.assert_called_once_with(*args, **kwargs)
|
||||
self.assertEqual(client.__enter__.call_count, 2)
|
||||
|
||||
def test_resize(self):
|
||||
size = 1024
|
||||
|
||||
|
@ -91,7 +179,7 @@ class RBDTestCase(test.NoDBTestCase):
|
|||
proxy = proxy_init.return_value
|
||||
proxy.__enter__.return_value = proxy
|
||||
self.driver.resize(self.volume_name, size)
|
||||
proxy.resize.assert_called_once_with(size * units.Ki)
|
||||
proxy.resize.assert_called_once_with(size)
|
||||
|
||||
def test_rbd_volume_proxy_init(self):
|
||||
with mock.patch.object(self.driver, '_connect_to_rados') as \
|
||||
|
@ -156,3 +244,27 @@ class RBDTestCase(test.NoDBTestCase):
|
|||
self.driver.ceph_conf = '/path/bar.conf'
|
||||
self.assertEqual(['--id', 'foo', '--conf', '/path/bar.conf'],
|
||||
self.driver.ceph_args())
|
||||
|
||||
def test_exists(self):
|
||||
snapshot = 'snap'
|
||||
|
||||
with mock.patch.object(rbd_utils, 'RBDVolumeProxy') as proxy_init:
|
||||
proxy = proxy_init.return_value
|
||||
self.assertTrue(self.driver.exists(self.volume_name,
|
||||
self.rbd_pool,
|
||||
snapshot))
|
||||
proxy.__enter__.assert_called_once()
|
||||
proxy.__exit__.assert_called_once()
|
||||
|
||||
def test_rename(self):
|
||||
new_name = 'bar'
|
||||
|
||||
with mock.patch.object(rbd_utils, 'RADOSClient') as client_init:
|
||||
client = client_init.return_value
|
||||
client.__enter__.return_value = client
|
||||
self.mock_rbd.RBD = mock.Mock(return_value=mock.Mock())
|
||||
self.driver.rename(self.volume_name, new_name)
|
||||
mock_rename = self.mock_rbd.RBD.return_value
|
||||
mock_rename.rename.assert_called_once_with(client.ioctx,
|
||||
self.volume_name,
|
||||
new_name)
|
||||
|
|
|
@ -204,8 +204,7 @@ class Image(object):
|
|||
'path': self.path})
|
||||
return can_fallocate
|
||||
|
||||
@staticmethod
|
||||
def verify_base_size(base, size, base_size=0):
|
||||
def verify_base_size(self, base, size, base_size=0):
|
||||
"""Check that the base image is not larger than size.
|
||||
Since images can't be generally shrunk, enforce this
|
||||
constraint taking account of virtual image size.
|
||||
|
@ -224,7 +223,7 @@ class Image(object):
|
|||
return
|
||||
|
||||
if size and not base_size:
|
||||
base_size = disk.get_disk_size(base)
|
||||
base_size = self.get_disk_size(base)
|
||||
|
||||
if size < base_size:
|
||||
msg = _('%(base)s virtual size %(base_size)s '
|
||||
|
@ -234,6 +233,9 @@ class Image(object):
|
|||
'size': size})
|
||||
raise exception.FlavorDiskTooSmall()
|
||||
|
||||
def get_disk_size(self, name):
|
||||
disk.get_disk_size(name)
|
||||
|
||||
def snapshot_extract(self, target, out_format):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -489,30 +491,35 @@ class Rbd(Image):
|
|||
return False
|
||||
|
||||
def check_image_exists(self):
|
||||
rbd_volumes = libvirt_utils.list_rbd_volumes(self.pool)
|
||||
for vol in rbd_volumes:
|
||||
if vol.startswith(self.rbd_name):
|
||||
return True
|
||||
return self.driver.exists(self.rbd_name)
|
||||
|
||||
return False
|
||||
def get_disk_size(self, name):
|
||||
"""Returns the size of the virtual disk in bytes.
|
||||
|
||||
The name argument is ignored since this backend already knows
|
||||
its name, and callers may pass a non-existent local file path.
|
||||
"""
|
||||
return self.driver.size(self.rbd_name)
|
||||
|
||||
def create_image(self, prepare_template, base, size, *args, **kwargs):
|
||||
if not os.path.exists(base):
|
||||
|
||||
if not self.check_image_exists():
|
||||
prepare_template(target=base, max_size=size, *args, **kwargs)
|
||||
else:
|
||||
self.verify_base_size(base, size)
|
||||
|
||||
# keep using the command line import instead of librbd since it
|
||||
# detects zeroes to preserve sparseness in the image
|
||||
args = ['--pool', self.pool, base, self.rbd_name]
|
||||
if self.driver.supports_layering():
|
||||
args += ['--new-format']
|
||||
args += self.driver.ceph_args()
|
||||
libvirt_utils.import_rbd_image(*args)
|
||||
# prepare_template() may have cloned the image into a new rbd
|
||||
# image already instead of downloading it locally
|
||||
if not self.check_image_exists():
|
||||
# keep using the command line import instead of librbd since it
|
||||
# detects zeroes to preserve sparseness in the image
|
||||
args = ['--pool', self.pool, base, self.rbd_name]
|
||||
if self.driver.supports_layering():
|
||||
args += ['--new-format']
|
||||
args += self.driver.ceph_args()
|
||||
libvirt_utils.import_rbd_image(*args)
|
||||
|
||||
base_size = disk.get_disk_size(base)
|
||||
|
||||
if size and size > base_size:
|
||||
if size and size > self.get_disk_size(self.rbd_name):
|
||||
self.driver.resize(self.rbd_name, size)
|
||||
|
||||
def snapshot_extract(self, target, out_format):
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
RBD clone image handler for libvirt hypervisors.
|
||||
"""
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from nova.openstack.common.gettextutils import _
|
||||
from nova.openstack.common import log as logging
|
||||
from nova.virt.imagehandler import base
|
||||
from nova.virt.libvirt import rbd_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_opt('images_rbd_pool', 'nova.virt.libvirt.imagebackend',
|
||||
group='libvirt')
|
||||
CONF.import_opt('images_rbd_ceph_conf', 'nova.virt.libvirt.imagebackend',
|
||||
group='libvirt')
|
||||
CONF.import_opt('rbd_user', 'nova.virt.libvirt.volume', group='libvirt')
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RBDCloneImageHandler(base.ImageHandler):
|
||||
"""Handler for rbd-backed images.
|
||||
|
||||
If libvirt is using rbd for ephemeral/root disks, this handler
|
||||
will clone images already stored in rbd instead of downloading
|
||||
them locally and importing them into rbd.
|
||||
|
||||
If rbd is not the image backend type, this handler does nothing.
|
||||
"""
|
||||
def __init__(self, driver=None, *args, **kwargs):
|
||||
super(RBDCloneImageHandler, self).__init__(driver, *args, **kwargs)
|
||||
if not CONF.libvirt.images_rbd_pool:
|
||||
raise RuntimeError(_('You should specify images_rbd_pool flag in '
|
||||
'the libvirt section to use rbd images.'))
|
||||
self.driver = rbd_utils.RBDDriver(
|
||||
pool=CONF.libvirt.images_rbd_pool,
|
||||
ceph_conf=CONF.libvirt.images_rbd_ceph_conf,
|
||||
rbd_user=CONF.libvirt.rbd_user,
|
||||
rbd_lib=kwargs.get('rbd'),
|
||||
rados_lib=kwargs.get('rados'))
|
||||
|
||||
def get_schemes(self):
|
||||
return ('rbd')
|
||||
|
||||
def is_local(self):
|
||||
return False
|
||||
|
||||
def _can_handle_image(self, context, image_meta, location, **kwargs):
|
||||
"""Returns whether it makes sense to clone the image.
|
||||
|
||||
- The glance image and libvirt image backend must be rbd.
|
||||
- The image must be readable by the rados user available to nova.
|
||||
- The image must be in raw format.
|
||||
"""
|
||||
backend_type = kwargs.get('backend_type')
|
||||
backend_location = kwargs.get('backend_location')
|
||||
if backend_type != 'rbd' or not backend_location:
|
||||
LOG.debug('backend type is not rbd or backend_location is not set')
|
||||
return False
|
||||
|
||||
return self.driver.is_cloneable(location['url'], image_meta)
|
||||
|
||||
def _fetch_image(self, context, image_id, image_meta, path,
|
||||
user_id=None, project_id=None, location=None,
|
||||
**kwargs):
|
||||
if not self._can_handle_image(context, image_meta, location, **kwargs):
|
||||
return False
|
||||
|
||||
dest_pool, dest_image = kwargs['backend_location']
|
||||
url = location['url']
|
||||
_fsid, pool, image, snapshot = self.driver.parse_location(url)
|
||||
self.driver.clone(dest_pool, dest_image, pool, image, snapshot)
|
||||
return True
|
||||
|
||||
def _remove_image(self, context, image_id, image_meta, path,
|
||||
user_id=None, project_id=None, location=None,
|
||||
**kwargs):
|
||||
if not self._can_handle_image(context, image_meta, location, **kwargs):
|
||||
return False
|
||||
|
||||
pool, image = kwargs['backend_location']
|
||||
self.driver.remove(image)
|
||||
return True
|
||||
|
||||
def _move_image(self, context, image_id, image_meta, src_path, dst_path,
|
||||
user_id=None, project_id=None, location=None,
|
||||
**kwargs):
|
||||
if not self._can_handle_image(context, image_meta, location, **kwargs):
|
||||
return False
|
||||
|
||||
src_pool, src_image = kwargs['backend_location']
|
||||
dest_image = kwargs.get('backend_dest')
|
||||
self.driver.rename(src_image, dest_image)
|
||||
return True
|
|
@ -11,6 +11,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import urllib
|
||||
|
||||
try:
|
||||
import rados
|
||||
import rbd
|
||||
|
@ -18,10 +20,11 @@ except ImportError:
|
|||
rados = None
|
||||
rbd = None
|
||||
|
||||
from nova import exception
|
||||
from nova.openstack.common import excutils
|
||||
from nova.openstack.common.gettextutils import _
|
||||
from nova.openstack.common import jsonutils
|
||||
from nova.openstack.common import log as logging
|
||||
from nova.openstack.common import units
|
||||
from nova import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -36,14 +39,23 @@ class RBDVolumeProxy(object):
|
|||
The underlying librados client and ioctx can be accessed as the attributes
|
||||
'client' and 'ioctx'.
|
||||
"""
|
||||
def __init__(self, driver, name, pool=None):
|
||||
def __init__(self, driver, name, pool=None, snapshot=None,
|
||||
read_only=False):
|
||||
client, ioctx = driver._connect_to_rados(pool)
|
||||
try:
|
||||
self.volume = driver.rbd.Image(ioctx, str(name), snapshot=None)
|
||||
snap_name = snapshot.encode('utf8') if snapshot else None
|
||||
self.volume = driver.rbd.Image(ioctx, name.encode('utf8'),
|
||||
snapshot=snap_name,
|
||||
read_only=read_only)
|
||||
except driver.rbd.ImageNotFound:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.debug("rbd image %s does not exist", name)
|
||||
driver._disconnect_from_rados(client, ioctx)
|
||||
except driver.rbd.Error:
|
||||
LOG.exception(_("error opening rbd image %s"), name)
|
||||
driver._disconnect_from_rados(client, ioctx)
|
||||
raise
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception(_("error opening rbd image %s"), name)
|
||||
driver._disconnect_from_rados(client, ioctx)
|
||||
|
||||
self.driver = driver
|
||||
self.client = client
|
||||
self.ioctx = ioctx
|
||||
|
@ -61,15 +73,17 @@ class RBDVolumeProxy(object):
|
|||
return getattr(self.volume, attrib)
|
||||
|
||||
|
||||
def ascii_str(s):
|
||||
"""Convert a string to ascii, or return None if the input is None.
|
||||
class RADOSClient(object):
|
||||
"""Context manager to simplify error handling for connecting to ceph."""
|
||||
def __init__(self, driver, pool=None):
|
||||
self.driver = driver
|
||||
self.cluster, self.ioctx = driver._connect_to_rados(pool)
|
||||
|
||||
This is useful when a parameter is None by default, or a string. LibRBD
|
||||
only accepts ascii, hence the need for conversion.
|
||||
"""
|
||||
if s is None:
|
||||
return s
|
||||
return str(s)
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type_, value, traceback):
|
||||
self.driver._disconnect_from_rados(self.cluster, self.ioctx)
|
||||
|
||||
|
||||
class RBDDriver(object):
|
||||
|
@ -89,8 +103,8 @@ class RBDDriver(object):
|
|||
conffile=self.ceph_conf)
|
||||
try:
|
||||
client.connect()
|
||||
pool_to_open = str(pool or self.pool)
|
||||
ioctx = client.open_ioctx(pool_to_open)
|
||||
pool_to_open = pool or self.pool
|
||||
ioctx = client.open_ioctx(pool_to_open.encode('utf-8'))
|
||||
return client, ioctx
|
||||
except self.rados.Error:
|
||||
# shutdown cannot raise an exception
|
||||
|
@ -130,12 +144,91 @@ class RBDDriver(object):
|
|||
ports.append(port)
|
||||
return hosts, ports
|
||||
|
||||
def parse_location(self, location):
|
||||
prefix = 'rbd://'
|
||||
if not location.startswith(prefix):
|
||||
reason = _('Not stored in rbd')
|
||||
raise exception.ImageUnacceptable(image_id=location, reason=reason)
|
||||
pieces = map(urllib.unquote, location[len(prefix):].split('/'))
|
||||
if '' in pieces:
|
||||
reason = _('Blank components')
|
||||
raise exception.ImageUnacceptable(image_id=location, reason=reason)
|
||||
if len(pieces) != 4:
|
||||
reason = _('Not an rbd snapshot')
|
||||
raise exception.ImageUnacceptable(image_id=location, reason=reason)
|
||||
return pieces
|
||||
|
||||
def _get_fsid(self):
|
||||
with RADOSClient(self) as client:
|
||||
return client.cluster.get_fsid()
|
||||
|
||||
def is_cloneable(self, image_location, image_meta):
|
||||
try:
|
||||
fsid, pool, image, snapshot = self.parse_location(image_location)
|
||||
except exception.ImageUnacceptable as e:
|
||||
LOG.debug(_('not cloneable: %s'), e)
|
||||
return False
|
||||
|
||||
if self._get_fsid() != fsid:
|
||||
reason = _('%s is in a different ceph cluster') % image_location
|
||||
LOG.debug(reason)
|
||||
return False
|
||||
|
||||
if image_meta['disk_format'] != 'raw':
|
||||
reason = _("rbd image clone requires image format to be "
|
||||
"'raw' but image {0} is '{1}'").format(
|
||||
image_location, image_meta['disk_format'])
|
||||
LOG.debug(reason)
|
||||
return False
|
||||
|
||||
# check that we can read the image
|
||||
try:
|
||||
return self.exists(image, pool=pool, snapshot=snapshot)
|
||||
except self.rbd.Error as e:
|
||||
LOG.debug(_('Unable to open image %(loc)s: %(err)s') %
|
||||
dict(loc=image_location, err=e))
|
||||
return False
|
||||
|
||||
def clone(self, dest_pool, dest_image, src_pool, src_image, src_snap):
|
||||
LOG.debug(_('cloning %(pool)s/%(img)s@%(snap)s to %(dstpl)s/%(dst)s') %
|
||||
dict(pool=src_pool, img=src_image, snap=src_snap,
|
||||
dst=dest_image, dstpl=dest_pool))
|
||||
with RADOSClient(self, src_pool) as src_client:
|
||||
with RADOSClient(self, dest_pool) as dest_client:
|
||||
self.rbd.RBD().clone(src_client.ioctx,
|
||||
src_image.encode('utf-8'),
|
||||
src_snap.encode('utf-8'),
|
||||
dest_client.ioctx,
|
||||
dest_image.encode('utf-8'),
|
||||
features=self.rbd.RBD_FEATURE_LAYERING)
|
||||
|
||||
def size(self, name):
|
||||
with RBDVolumeProxy(self, name) as vol:
|
||||
return vol.size()
|
||||
|
||||
def resize(self, volume_name, size):
|
||||
size = int(size) * units.Ki
|
||||
def resize(self, name, size_bytes):
|
||||
LOG.debug('resizing rbd image %s to %d', name, size_bytes)
|
||||
with RBDVolumeProxy(self, name) as vol:
|
||||
vol.resize(size_bytes)
|
||||
|
||||
with RBDVolumeProxy(self, volume_name) as vol:
|
||||
vol.resize(size)
|
||||
def exists(self, name, pool=None, snapshot=None):
|
||||
try:
|
||||
with RBDVolumeProxy(self, name,
|
||||
pool=pool,
|
||||
snapshot=snapshot,
|
||||
read_only=True):
|
||||
return True
|
||||
except self.rbd.ImageNotFound:
|
||||
return False
|
||||
|
||||
def remove(self, name):
|
||||
LOG.debug('removing rbd image %s', name)
|
||||
with RBDVolumeProxy(self, name) as vol:
|
||||
vol.remove()
|
||||
|
||||
def rename(self, name, new_name):
|
||||
LOG.debug('renaming rbd image %s to %s', name, new_name)
|
||||
with RADOSClient(self) as client:
|
||||
self.rbd.RBD().rename(client.ioctx,
|
||||
name.encode('utf-8'),
|
||||
new_name.encode('utf-8'))
|
||||
|
|
|
@ -31,6 +31,7 @@ nova.image.download.modules =
|
|||
file = nova.image.download.file
|
||||
nova.virt.image.handlers =
|
||||
download = nova.virt.imagehandler.download:DownloadImageHandler
|
||||
libvirt_rbd_clone = nova.virt.libvirt.imagehandler:RBDCloneImageHandler
|
||||
console_scripts =
|
||||
nova-all = nova.cmd.all:main
|
||||
nova-api = nova.cmd.api:main
|
||||
|
|
Loading…
Reference in New Issue