Merge "enable cloning for rbd-backed ephemeral disks"
This commit is contained in:
@@ -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
|
||||
+113
-20
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user