Merge "Move libvirt RBD utilities to a new file"

This commit is contained in:
Jenkins
2014-07-27 23:33:16 +00:00
committed by Gerrit Code Review
4 changed files with 340 additions and 146 deletions

View File

@@ -18,7 +18,6 @@ import shutil
import tempfile import tempfile
import fixtures import fixtures
import mock
from oslo.config import cfg from oslo.config import cfg
import inspect import inspect
@@ -30,6 +29,7 @@ from nova import test
from nova.tests import fake_processutils from nova.tests import fake_processutils
from nova.tests.virt.libvirt import fake_libvirt_utils from nova.tests.virt.libvirt import fake_libvirt_utils
from nova.virt.libvirt import imagebackend from nova.virt.libvirt import imagebackend
from nova.virt.libvirt import rbd
CONF = cfg.CONF CONF = cfg.CONF
@@ -671,14 +671,8 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
group='libvirt') group='libvirt')
self.libvirt_utils = imagebackend.libvirt_utils self.libvirt_utils = imagebackend.libvirt_utils
self.utils = imagebackend.utils self.utils = imagebackend.utils
self.rbd = self.mox.CreateMockAnything() self.mox.StubOutWithMock(rbd, 'rbd')
self.rados = self.mox.CreateMockAnything() self.mox.StubOutWithMock(rbd, 'rados')
def prepare_mocks(self):
fn = self.mox.CreateMockAnything()
self.mox.StubOutWithMock(imagebackend, 'rbd')
self.mox.StubOutWithMock(imagebackend, 'rados')
return fn
def test_cache(self): def test_cache(self):
image = self.image_class(self.INSTANCE, self.NAME) image = self.image_class(self.INSTANCE, self.NAME)
@@ -746,10 +740,10 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
self.mox.VerifyAll() self.mox.VerifyAll()
def test_create_image(self): def test_create_image(self):
fn = self.prepare_mocks() fn = self.mox.CreateMockAnything()
fn(max_size=None, rbd=self.rbd, target=self.TEMPLATE_PATH) fn(max_size=None, target=self.TEMPLATE_PATH)
self.rbd.RBD_FEATURE_LAYERING = 1 rbd.rbd.RBD_FEATURE_LAYERING = 1
self.mox.StubOutWithMock(imagebackend.disk, 'get_disk_size') self.mox.StubOutWithMock(imagebackend.disk, 'get_disk_size')
imagebackend.disk.get_disk_size(self.TEMPLATE_PATH imagebackend.disk.get_disk_size(self.TEMPLATE_PATH
@@ -762,7 +756,7 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
self.mox.ReplayAll() self.mox.ReplayAll()
image = self.image_class(self.INSTANCE, self.NAME) image = self.image_class(self.INSTANCE, self.NAME)
image.create_image(fn, self.TEMPLATE_PATH, None, rbd=self.rbd) image.create_image(fn, self.TEMPLATE_PATH, None)
self.mox.VerifyAll() self.mox.VerifyAll()
@@ -771,8 +765,6 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
fake_processutils.fake_execute_clear_log() fake_processutils.fake_execute_clear_log()
fake_processutils.stub_out_processutils_execute(self.stubs) fake_processutils.stub_out_processutils_execute(self.stubs)
self.mox.StubOutWithMock(imagebackend, 'rbd')
self.mox.StubOutWithMock(imagebackend, 'rados')
image = self.image_class(self.INSTANCE, self.NAME) image = self.image_class(self.INSTANCE, self.NAME)
def fake_fetch(target, *args, **kwargs): def fake_fetch(target, *args, **kwargs):
@@ -807,16 +799,6 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
self.assertEqual(image.path, rbd_path) self.assertEqual(image.path, rbd_path)
def test_resize(self):
image = self.image_class(self.INSTANCE, self.NAME)
with mock.patch.object(imagebackend, "RBDVolumeProxy") as mock_proxy:
volume_mock = mock.Mock()
mock_proxy.side_effect = [mock_proxy]
mock_proxy.__enter__.side_effect = [volume_mock]
image._resize(image.rbd_name, self.SIZE)
volume_mock.resize.assert_called_once_with(self.SIZE)
class BackendTestCase(test.NoDBTestCase): class BackendTestCase(test.NoDBTestCase):
INSTANCE = {'name': 'fake-instance', INSTANCE = {'name': 'fake-instance',
@@ -859,6 +841,8 @@ class BackendTestCase(test.NoDBTestCase):
pool = "FakePool" pool = "FakePool"
self.flags(images_rbd_pool=pool, group='libvirt') self.flags(images_rbd_pool=pool, group='libvirt')
self.flags(images_rbd_ceph_conf=conf, group='libvirt') self.flags(images_rbd_ceph_conf=conf, group='libvirt')
self.mox.StubOutWithMock(rbd, 'rbd')
self.mox.StubOutWithMock(rbd, 'rados')
self._test_image('rbd', imagebackend.Rbd, imagebackend.Rbd) self._test_image('rbd', imagebackend.Rbd, imagebackend.Rbd)
def test_image_default(self): def test_image_default(self):

View File

@@ -0,0 +1,169 @@
# 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.openstack.common import log as logging
from nova import test
from nova import utils
from nova.virt.libvirt import rbd
LOG = logging.getLogger(__name__)
CEPH_MON_DUMP = """dumped monmap epoch 1
{ "epoch": 1,
"fsid": "33630410-6d93-4d66-8e42-3b953cf194aa",
"modified": "2013-05-22 17:44:56.343618",
"created": "2013-05-22 17:44:56.343618",
"mons": [
{ "rank": 0,
"name": "a",
"addr": "[::1]:6789\/0"},
{ "rank": 1,
"name": "b",
"addr": "[::1]:6790\/0"},
{ "rank": 2,
"name": "c",
"addr": "[::1]:6791\/0"},
{ "rank": 3,
"name": "d",
"addr": "127.0.0.1:6792\/0"},
{ "rank": 4,
"name": "e",
"addr": "example.com:6791\/0"}],
"quorum": [
0,
1,
2]}
"""
class RbdTestCase(test.NoDBTestCase):
@mock.patch.object(rbd, 'rbd')
@mock.patch.object(rbd, 'rados')
def setUp(self, mock_rados, mock_rbd):
super(RbdTestCase, self).setUp()
self.mock_rados = mock_rados
self.mock_rados.Rados = mock.Mock
self.mock_rados.Rados.ioctx = mock.Mock()
self.mock_rados.Rados.connect = mock.Mock()
self.mock_rados.Rados.shutdown = mock.Mock()
self.mock_rados.Rados.open_ioctx = mock.Mock()
self.mock_rados.Rados.open_ioctx.return_value = \
self.mock_rados.Rados.ioctx
self.mock_rados.Error = Exception
self.mock_rbd = mock_rbd
self.mock_rbd.RBD = mock.Mock
self.mock_rbd.Image = mock.Mock
self.mock_rbd.Image.close = mock.Mock()
self.mock_rbd.RBD.Error = Exception
self.rbd_pool = 'rbd'
self.driver = rbd.RBDDriver(self.rbd_pool, None, None)
self.volume_name = u'volume-00000001'
def tearDown(self):
super(RbdTestCase, self).tearDown()
@mock.patch.object(utils, 'execute')
def test_get_mon_addrs(self, mock_execute):
mock_execute.return_value = (CEPH_MON_DUMP, '')
hosts = ['::1', '::1', '::1', '127.0.0.1', 'example.com']
ports = ['6789', '6790', '6791', '6792', '6791']
self.assertEqual((hosts, ports), self.driver.get_mon_addrs())
@mock.patch.object(rbd, 'RBDVolumeProxy')
def test_resize(self, mock_proxy):
size = 1024
proxy = mock_proxy.return_value
proxy.__enter__.return_value = proxy
self.driver.resize(self.volume_name, size)
proxy.resize.assert_called_once_with(size)
@mock.patch.object(rbd.RBDDriver, '_disconnect_from_rados')
@mock.patch.object(rbd.RBDDriver, '_connect_to_rados')
@mock.patch.object(rbd, 'rbd')
@mock.patch.object(rbd, 'rados')
def test_rbd_volume_proxy_init(self, mock_rados, mock_rbd,
mock_connect_from_rados,
mock_disconnect_from_rados):
mock_connect_from_rados.return_value = (None, None)
mock_disconnect_from_rados.return_value = (None, None)
with rbd.RBDVolumeProxy(self.driver, self.volume_name):
mock_connect_from_rados.assert_called_once_with(None)
self.assertFalse(mock_disconnect_from_rados.called)
mock_disconnect_from_rados.assert_called_once_with(None, None)
@mock.patch.object(rbd, 'rbd')
@mock.patch.object(rbd, 'rados')
def test_connect_to_rados_default(self, mock_rados, mock_rbd):
ret = self.driver._connect_to_rados()
self.assertTrue(self.mock_rados.Rados.connect.called)
self.assertTrue(self.mock_rados.Rados.open_ioctx.called)
self.assertIsInstance(ret[0], self.mock_rados.Rados)
self.assertEqual(ret[1], self.mock_rados.Rados.ioctx)
self.mock_rados.Rados.open_ioctx.assert_called_with(self.rbd_pool)
@mock.patch.object(rbd, 'rbd')
@mock.patch.object(rbd, 'rados')
def test_connect_to_rados_different_pool(self, mock_rados, mock_rbd):
ret = self.driver._connect_to_rados('alt_pool')
self.assertTrue(self.mock_rados.Rados.connect.called)
self.assertTrue(self.mock_rados.Rados.open_ioctx.called)
self.assertIsInstance(ret[0], self.mock_rados.Rados)
self.assertEqual(ret[1], self.mock_rados.Rados.ioctx)
self.mock_rados.Rados.open_ioctx.assert_called_with('alt_pool')
@mock.patch.object(rbd, 'rados')
def test_connect_to_rados_error(self, mock_rados):
mock_rados.Rados.open_ioctx.side_effect = mock_rados.Error
self.assertRaises(mock_rados.Error, self.driver._connect_to_rados)
mock_rados.Rados.open_ioctx.assert_called_once_with(self.rbd_pool)
mock_rados.Rados.shutdown.assert_called_once_with()
def test_ceph_args_none(self):
self.driver.rbd_user = None
self.driver.ceph_conf = None
self.assertEqual([], self.driver.ceph_args())
def test_ceph_args_rbd_user(self):
self.driver.rbd_user = 'foo'
self.driver.ceph_conf = None
self.assertEqual(['--id', 'foo'], self.driver.ceph_args())
def test_ceph_args_ceph_conf(self):
self.driver.rbd_user = None
self.driver.ceph_conf = '/path/bar.conf'
self.assertEqual(['--conf', '/path/bar.conf'],
self.driver.ceph_args())
def test_ceph_args_rbd_user_and_ceph_conf(self):
self.driver.rbd_user = 'foo'
self.driver.ceph_conf = '/path/bar.conf'
self.assertEqual(['--id', 'foo', '--conf', '/path/bar.conf'],
self.driver.ceph_args())
@mock.patch.object(rbd, 'RBDVolumeProxy')
def test_exists(self, mock_proxy):
proxy = mock_proxy.return_value
self.assertTrue(self.driver.exists(self.volume_name))
proxy.__enter__.assert_called_once_with()
proxy.__exit__.assert_called_once_with(None, None, None)

View File

@@ -33,17 +33,9 @@ from nova.virt.disk import api as disk
from nova.virt import images from nova.virt import images
from nova.virt.libvirt import config as vconfig from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import lvm from nova.virt.libvirt import lvm
from nova.virt.libvirt import rbd
from nova.virt.libvirt import utils as libvirt_utils from nova.virt.libvirt import utils as libvirt_utils
try:
import rados
import rbd
except ImportError:
rados = None
rbd = None
__imagebackend_opts = [ __imagebackend_opts = [
cfg.StrOpt('images_type', cfg.StrOpt('images_type',
default='default', default='default',
@@ -76,6 +68,8 @@ CONF = cfg.CONF
CONF.register_opts(__imagebackend_opts, 'libvirt') CONF.register_opts(__imagebackend_opts, 'libvirt')
CONF.import_opt('image_cache_subdirectory_name', 'nova.virt.imagecache') CONF.import_opt('image_cache_subdirectory_name', 'nova.virt.imagecache')
CONF.import_opt('preallocate_images', 'nova.virt.driver') CONF.import_opt('preallocate_images', 'nova.virt.driver')
CONF.import_opt('rbd_user', 'nova.virt.libvirt.volume', group='libvirt')
CONF.import_opt('rbd_secret_uuid', 'nova.virt.libvirt.volume', group='libvirt')
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -488,51 +482,6 @@ class Lvm(Image):
run_as_root=True) run_as_root=True)
class RBDVolumeProxy(object):
"""Context manager for dealing with an existing rbd volume.
This handles connecting to rados and opening an ioctx automatically, and
otherwise acts like a librbd Image object.
The underlying librados client and ioctx can be accessed as the attributes
'client' and 'ioctx'.
"""
def __init__(self, driver, name, pool=None):
client, ioctx = driver._connect_to_rados(pool)
try:
self.volume = driver.rbd.Image(ioctx, str(name), snapshot=None)
except driver.rbd.Error:
LOG.exception(_LE("error opening rbd image %s"), name)
driver._disconnect_from_rados(client, ioctx)
raise
self.driver = driver
self.client = client
self.ioctx = ioctx
def __enter__(self):
return self
def __exit__(self, type_, value, traceback):
try:
self.volume.close()
finally:
self.driver._disconnect_from_rados(self.client, self.ioctx)
def __getattr__(self, attrib):
return getattr(self.volume, attrib)
def ascii_str(s):
"""Convert a string to ascii, or return None if the input is None.
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)
class Rbd(Image): class Rbd(Image):
def __init__(self, instance=None, disk_name=None, path=None, **kwargs): def __init__(self, instance=None, disk_name=None, path=None, **kwargs):
super(Rbd, self).__init__("block", "rbd", is_block_dev=True) super(Rbd, self).__init__("block", "rbd", is_block_dev=True)
@@ -549,10 +498,13 @@ class Rbd(Image):
' images_rbd_pool' ' images_rbd_pool'
' flag to use rbd images.')) ' flag to use rbd images.'))
self.pool = CONF.libvirt.images_rbd_pool self.pool = CONF.libvirt.images_rbd_pool
self.ceph_conf = ascii_str(CONF.libvirt.images_rbd_ceph_conf) self.rbd_user = CONF.libvirt.rbd_user
self.rbd_user = ascii_str(CONF.libvirt.rbd_user) self.ceph_conf = CONF.libvirt.images_rbd_ceph_conf
self.rbd = kwargs.get('rbd', rbd)
self.rados = kwargs.get('rados', rados) self.driver = rbd.RBDDriver(
pool=self.pool,
ceph_conf=self.ceph_conf,
rbd_user=self.rbd_user)
self.path = 'rbd:%s/%s' % (self.pool, self.rbd_name) self.path = 'rbd:%s/%s' % (self.pool, self.rbd_name)
if self.rbd_user: if self.rbd_user:
@@ -560,52 +512,6 @@ class Rbd(Image):
if self.ceph_conf: if self.ceph_conf:
self.path += ':conf=' + self.ceph_conf self.path += ':conf=' + self.ceph_conf
def _connect_to_rados(self, pool=None):
client = self.rados.Rados(rados_id=self.rbd_user,
conffile=self.ceph_conf)
try:
client.connect()
pool_to_open = str(pool or self.pool)
ioctx = client.open_ioctx(pool_to_open)
return client, ioctx
except self.rados.Error:
# shutdown cannot raise an exception
client.shutdown()
raise
def _disconnect_from_rados(self, client, ioctx):
# closing an ioctx cannot raise an exception
ioctx.close()
client.shutdown()
def _supports_layering(self):
return hasattr(self.rbd, 'RBD_FEATURE_LAYERING')
def _ceph_args(self):
args = []
if self.rbd_user:
args.extend(['--id', self.rbd_user])
if self.ceph_conf:
args.extend(['--conf', self.ceph_conf])
return args
def _get_mon_addrs(self):
args = ['ceph', 'mon', 'dump', '--format=json'] + self._ceph_args()
out, _ = utils.execute(*args)
lines = out.split('\n')
if lines[0].startswith('dumped monmap epoch'):
lines = lines[1:]
monmap = jsonutils.loads('\n'.join(lines))
addrs = [mon['addr'] for mon in monmap['mons']]
hosts = []
ports = []
for addr in addrs:
host_port = addr[:addr.rindex('/')]
host, port = host_port.rsplit(':', 1)
hosts.append(host.strip('[]'))
ports.append(port)
return hosts, ports
def libvirt_info(self, disk_bus, disk_dev, device_type, cache_mode, def libvirt_info(self, disk_bus, disk_dev, device_type, cache_mode,
extra_specs, hypervisor_version): extra_specs, hypervisor_version):
"""Get `LibvirtConfigGuestDisk` filled for this image. """Get `LibvirtConfigGuestDisk` filled for this image.
@@ -618,7 +524,7 @@ class Rbd(Image):
""" """
info = vconfig.LibvirtConfigGuestDisk() info = vconfig.LibvirtConfigGuestDisk()
hosts, ports = self._get_mon_addrs() hosts, ports = self.driver.get_mon_addrs()
info.device_type = device_type info.device_type = device_type
info.driver_format = 'raw' info.driver_format = 'raw'
info.driver_cache = cache_mode info.driver_cache = cache_mode
@@ -644,21 +550,9 @@ class Rbd(Image):
return False return False
def check_image_exists(self): def check_image_exists(self):
rbd_volumes = libvirt_utils.list_rbd_volumes(self.pool) return self.driver.exists(self.rbd_name)
for vol in rbd_volumes:
if vol.startswith(self.rbd_name):
return True
return False
def _resize(self, volume_name, size):
with RBDVolumeProxy(self, volume_name) as vol:
vol.resize(int(size))
def create_image(self, prepare_template, base, size, *args, **kwargs): def create_image(self, prepare_template, base, size, *args, **kwargs):
if self.rbd is None:
raise RuntimeError(_('rbd python libraries not found'))
if not os.path.exists(base): if not os.path.exists(base):
prepare_template(target=base, max_size=size, *args, **kwargs) prepare_template(target=base, max_size=size, *args, **kwargs)
else: else:
@@ -667,15 +561,15 @@ class Rbd(Image):
# keep using the command line import instead of librbd since it # keep using the command line import instead of librbd since it
# detects zeroes to preserve sparseness in the image # detects zeroes to preserve sparseness in the image
args = ['--pool', self.pool, base, self.rbd_name] args = ['--pool', self.pool, base, self.rbd_name]
if self._supports_layering(): if self.driver.supports_layering():
args += ['--new-format'] args += ['--new-format']
args += self._ceph_args() args += self.driver.ceph_args()
libvirt_utils.import_rbd_image(*args) libvirt_utils.import_rbd_image(*args)
base_size = disk.get_disk_size(base) base_size = disk.get_disk_size(base)
if size and size > base_size: if size and size > base_size:
self._resize(self.rbd_name, size) self.driver.resize(self.rbd_name, size)
def snapshot_extract(self, target, out_format): def snapshot_extract(self, target, out_format):
images.convert_image(self.path, target, out_format) images.convert_image(self.path, target, out_format)

147
nova/virt/libvirt/rbd.py Normal file
View File

@@ -0,0 +1,147 @@
# Copyright 2012 Grid Dynamics
# Copyright 2013 Inktank Storage, Inc.
# Copyright 2014 Mirantis, Inc.
#
# 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.
try:
import rados
import rbd
except ImportError:
rados = None
rbd = None
from nova.i18n import _
from nova.i18n import _LE
from nova.openstack.common import jsonutils
from nova.openstack.common import log as logging
from nova import utils
LOG = logging.getLogger(__name__)
class RBDVolumeProxy(object):
"""Context manager for dealing with an existing rbd volume.
This handles connecting to rados and opening an ioctx automatically, and
otherwise acts like a librbd Image object.
The underlying librados client and ioctx can be accessed as the attributes
'client' and 'ioctx'.
"""
def __init__(self, driver, name, pool=None):
client, ioctx = driver._connect_to_rados(pool)
try:
self.volume = rbd.Image(ioctx, str(name), snapshot=None)
except rbd.Error:
LOG.exception(_LE("error opening rbd image %s"), name)
driver._disconnect_from_rados(client, ioctx)
raise
self.driver = driver
self.client = client
self.ioctx = ioctx
def __enter__(self):
return self
def __exit__(self, type_, value, traceback):
try:
self.volume.close()
finally:
self.driver._disconnect_from_rados(self.client, self.ioctx)
def __getattr__(self, attrib):
return getattr(self.volume, attrib)
class RBDDriver(object):
def __init__(self, pool, ceph_conf, rbd_user):
self.pool = pool.encode('utf8')
# NOTE(angdraug): rados.Rados fails to connect if ceph_conf is None:
# https://github.com/ceph/ceph/pull/1787
self.ceph_conf = ceph_conf.encode('utf8') if ceph_conf else ''
self.rbd_user = rbd_user.encode('utf8') if rbd_user else None
if rbd is None:
raise RuntimeError(_('rbd python libraries not found'))
def _connect_to_rados(self, pool=None):
client = rados.Rados(rados_id=self.rbd_user,
conffile=self.ceph_conf)
try:
client.connect()
pool_to_open = str(pool or self.pool)
ioctx = client.open_ioctx(pool_to_open)
return client, ioctx
except rados.Error:
# shutdown cannot raise an exception
client.shutdown()
raise
def _disconnect_from_rados(self, client, ioctx):
# closing an ioctx cannot raise an exception
ioctx.close()
client.shutdown()
def supports_layering(self):
return hasattr(rbd, 'RBD_FEATURE_LAYERING')
def ceph_args(self):
"""List of command line parameters to be passed to ceph commands to
reflect RBDDriver configuration such as RBD user name and location
of ceph.conf.
"""
args = []
if self.rbd_user:
args.extend(['--id', self.rbd_user])
if self.ceph_conf:
args.extend(['--conf', self.ceph_conf])
return args
def get_mon_addrs(self):
args = ['ceph', 'mon', 'dump', '--format=json'] + self.ceph_args()
out, _ = utils.execute(*args)
lines = out.split('\n')
if lines[0].startswith('dumped monmap epoch'):
lines = lines[1:]
monmap = jsonutils.loads('\n'.join(lines))
addrs = [mon['addr'] for mon in monmap['mons']]
hosts = []
ports = []
for addr in addrs:
host_port = addr[:addr.rindex('/')]
host, port = host_port.rsplit(':', 1)
hosts.append(host.strip('[]'))
ports.append(port)
return hosts, ports
def size(self, name):
with RBDVolumeProxy(self, name) as vol:
return vol.size()
def resize(self, name, size):
"""Resize RBD volume.
:name: Name of RBD object
:size: New size in bytes
"""
LOG.debug('resizing rbd image %s to %d', name, size)
with RBDVolumeProxy(self, name) as vol:
vol.resize(size)
def exists(self, name):
try:
with RBDVolumeProxy(self, name):
return True
except rbd.ImageNotFound:
return False