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 fixtures
import mock
from oslo.config import cfg
import inspect
@@ -30,6 +29,7 @@ from nova import test
from nova.tests import fake_processutils
from nova.tests.virt.libvirt import fake_libvirt_utils
from nova.virt.libvirt import imagebackend
from nova.virt.libvirt import rbd
CONF = cfg.CONF
@@ -671,14 +671,8 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
group='libvirt')
self.libvirt_utils = imagebackend.libvirt_utils
self.utils = imagebackend.utils
self.rbd = self.mox.CreateMockAnything()
self.rados = self.mox.CreateMockAnything()
def prepare_mocks(self):
fn = self.mox.CreateMockAnything()
self.mox.StubOutWithMock(imagebackend, 'rbd')
self.mox.StubOutWithMock(imagebackend, 'rados')
return fn
self.mox.StubOutWithMock(rbd, 'rbd')
self.mox.StubOutWithMock(rbd, 'rados')
def test_cache(self):
image = self.image_class(self.INSTANCE, self.NAME)
@@ -746,10 +740,10 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
self.mox.VerifyAll()
def test_create_image(self):
fn = self.prepare_mocks()
fn(max_size=None, rbd=self.rbd, target=self.TEMPLATE_PATH)
fn = self.mox.CreateMockAnything()
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')
imagebackend.disk.get_disk_size(self.TEMPLATE_PATH
@@ -762,7 +756,7 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
self.mox.ReplayAll()
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()
@@ -771,8 +765,6 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
fake_processutils.fake_execute_clear_log()
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)
def fake_fetch(target, *args, **kwargs):
@@ -807,16 +799,6 @@ class RbdTestCase(_ImageTestCase, test.NoDBTestCase):
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):
INSTANCE = {'name': 'fake-instance',
@@ -859,6 +841,8 @@ class BackendTestCase(test.NoDBTestCase):
pool = "FakePool"
self.flags(images_rbd_pool=pool, 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)
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.libvirt import config as vconfig
from nova.virt.libvirt import lvm
from nova.virt.libvirt import rbd
from nova.virt.libvirt import utils as libvirt_utils
try:
import rados
import rbd
except ImportError:
rados = None
rbd = None
__imagebackend_opts = [
cfg.StrOpt('images_type',
default='default',
@@ -76,6 +68,8 @@ CONF = cfg.CONF
CONF.register_opts(__imagebackend_opts, 'libvirt')
CONF.import_opt('image_cache_subdirectory_name', 'nova.virt.imagecache')
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__)
@@ -488,51 +482,6 @@ class Lvm(Image):
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):
def __init__(self, instance=None, disk_name=None, path=None, **kwargs):
super(Rbd, self).__init__("block", "rbd", is_block_dev=True)
@@ -549,10 +498,13 @@ class Rbd(Image):
' images_rbd_pool'
' flag to use rbd images.'))
self.pool = CONF.libvirt.images_rbd_pool
self.ceph_conf = ascii_str(CONF.libvirt.images_rbd_ceph_conf)
self.rbd_user = ascii_str(CONF.libvirt.rbd_user)
self.rbd = kwargs.get('rbd', rbd)
self.rados = kwargs.get('rados', rados)
self.rbd_user = CONF.libvirt.rbd_user
self.ceph_conf = CONF.libvirt.images_rbd_ceph_conf
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)
if self.rbd_user:
@@ -560,52 +512,6 @@ class Rbd(Image):
if 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,
extra_specs, hypervisor_version):
"""Get `LibvirtConfigGuestDisk` filled for this image.
@@ -618,7 +524,7 @@ class Rbd(Image):
"""
info = vconfig.LibvirtConfigGuestDisk()
hosts, ports = self._get_mon_addrs()
hosts, ports = self.driver.get_mon_addrs()
info.device_type = device_type
info.driver_format = 'raw'
info.driver_cache = cache_mode
@@ -644,21 +550,9 @@ 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 False
def _resize(self, volume_name, size):
with RBDVolumeProxy(self, volume_name) as vol:
vol.resize(int(size))
return self.driver.exists(self.rbd_name)
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):
prepare_template(target=base, max_size=size, *args, **kwargs)
else:
@@ -667,15 +561,15 @@ class Rbd(Image):
# 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._supports_layering():
if self.driver.supports_layering():
args += ['--new-format']
args += self._ceph_args()
args += self.driver.ceph_args()
libvirt_utils.import_rbd_image(*args)
base_size = disk.get_disk_size(base)
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):
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