Libvirt: Created Nova driver for Quobyte

Driver for Quobyte storage product.
Mounts respective Quobyte volume of VM images.
This is the Nova part of integrating Quobyte.
Corresponding Cinder Driver Blueprint:
https://blueprints.launchpad.net/cinder/+spec/quobyte-usp-driver
The Cinder driver has been merged into Cinder,
it's review can be found at:
https://review.openstack.org/#/c/94186/

blueprint quobyte-nova-driver
DocImpact: Adding option 'quobyte_mount_point_base' and 'quobyte_client_cfg'

Change-Id: Ica1820031f1fc8b66d7ed7fe76ffeb985cf0ef35
This commit is contained in:
Silvan Kaiser 2014-12-18 17:43:33 +01:00
parent bb1a6b6dda
commit 9f7b1b0d99
5 changed files with 528 additions and 0 deletions

View File

@ -0,0 +1,183 @@
# Copyright (c) 2015 Quobyte Inc.
# All Rights Reserved.
#
# 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.
"""Unit tests for the Quobyte volume driver module."""
import mock
import os
from oslo_concurrency import processutils
from nova import exception
from nova.openstack.common import fileutils
from nova import test
from nova import utils
from nova.virt.libvirt import quobyte
class QuobyteTestCase(test.NoDBTestCase):
@mock.patch.object(fileutils, "ensure_tree")
@mock.patch.object(utils, "execute")
def test_quobyte_mount_volume(self, mock_execute, mock_ensure_tree):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
quobyte.mount_volume(quobyte_volume, export_mnt_base)
mock_ensure_tree.assert_called_once_with(export_mnt_base)
expected_commands = [mock.call('mount.quobyte',
quobyte_volume,
export_mnt_base,
check_exit_code=[0, 4])
]
mock_execute.assert_has_calls(expected_commands)
@mock.patch.object(fileutils, "ensure_tree")
@mock.patch.object(utils, "execute")
def test_quobyte_mount_volume_with_config(self,
mock_execute,
mock_ensure_tree):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
config_file_dummy = "/etc/quobyte/dummy.conf"
quobyte.mount_volume(quobyte_volume,
export_mnt_base,
config_file_dummy)
mock_ensure_tree.assert_called_once_with(export_mnt_base)
expected_commands = [mock.call('mount.quobyte',
quobyte_volume,
export_mnt_base,
'-c',
config_file_dummy,
check_exit_code=[0, 4])
]
mock_execute.assert_has_calls(expected_commands)
@mock.patch.object(fileutils, "ensure_tree")
@mock.patch.object(utils, "execute",
side_effect=(processutils.
ProcessExecutionError))
def test_quobyte_mount_volume_fails(self, mock_execute, mock_ensure_tree):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
self.assertRaises(processutils.ProcessExecutionError,
quobyte.mount_volume,
quobyte_volume,
export_mnt_base)
@mock.patch.object(utils, "execute")
def test_quobyte_umount_volume(self, mock_execute):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
quobyte.umount_volume(export_mnt_base)
mock_execute.assert_called_once_with('umount.quobyte',
export_mnt_base)
@mock.patch.object(quobyte.LOG, "error")
@mock.patch.object(utils, "execute")
def test_quobyte_umount_volume_warns(self,
mock_execute,
mock_debug):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
def exec_side_effect(*cmd, **kwargs):
exerror = processutils.ProcessExecutionError()
exerror.message = "Device or resource busy"
raise exerror
mock_execute.side_effect = exec_side_effect
quobyte.umount_volume(export_mnt_base)
(mock_debug.
assert_called_once_with("The Quobyte volume at %s is still in use.",
export_mnt_base))
@mock.patch.object(quobyte.LOG, "exception")
@mock.patch.object(utils, "execute",
side_effect=(processutils.ProcessExecutionError))
def test_quobyte_umount_volume_fails(self,
mock_execute,
mock_exception):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
quobyte.umount_volume(export_mnt_base)
(mock_exception.
assert_called_once_with("Couldn't unmount "
"the Quobyte Volume at %s",
export_mnt_base))
@mock.patch.object(os, "access", return_value=True)
@mock.patch.object(utils, "execute")
def test_quobyte_is_valid_volume(self, mock_execute, mock_access):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
quobyte.validate_volume(export_mnt_base)
mock_execute.assert_called_once_with('getfattr',
'-n',
'quobyte.info',
export_mnt_base)
@mock.patch.object(utils, "execute",
side_effect=(processutils.
ProcessExecutionError))
def test_quobyte_is_valid_volume_vol_not_valid_volume(self, mock_execute):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
self.assertRaises(exception.NovaException,
quobyte.validate_volume,
export_mnt_base)
@mock.patch.object(os, "access", return_value=False)
@mock.patch.object(utils, "execute",
side_effect=(processutils.
ProcessExecutionError))
def test_quobyte_is_valid_volume_vol_no_valid_access(self,
mock_execute,
mock_access):
mnt_base = '/mnt'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
self.assertRaises(exception.NovaException,
quobyte.validate_volume,
export_mnt_base)

View File

@ -31,6 +31,7 @@ from nova.tests.unit.virt.libvirt import fake_libvirt_utils
from nova.tests.unit.virt.libvirt import fakelibvirt
from nova import utils
from nova.virt.libvirt import host
from nova.virt.libvirt import quobyte
from nova.virt.libvirt import utils as libvirt_utils
from nova.virt.libvirt import volume
@ -1472,3 +1473,177 @@ Setting up iSCSI targets: unused
tree = conf.format_dom()
self.assertEqual('file', tree.get('type'))
self.assertEqual('fake_serial', tree.find('./serial').text)
@mock.patch.object(quobyte, 'validate_volume')
@mock.patch.object(quobyte, 'mount_volume')
@mock.patch.object(libvirt_utils, 'is_mounted', return_value=False)
def test_libvirt_quobyte_driver_mount(self,
mock_is_mounted,
mock_mount_volume,
mock_validate_volume
):
mnt_base = '/mnt'
self.flags(quobyte_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtQuobyteVolumeDriver(self.fake_conn)
export_string = 'quobyte://192.168.1.1/volume-00001'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
file_path = os.path.join(export_mnt_base, self.name)
connection_info = {'data': {'export': export_string,
'name': self.name}}
libvirt_driver.connect_volume(connection_info, self.disk_info)
conf = libvirt_driver.get_config(connection_info, self.disk_info)
tree = conf.format_dom()
self._assertFileTypeEquals(tree, file_path)
mock_mount_volume.assert_called_once_with(quobyte_volume,
export_mnt_base,
mock.ANY)
mock_validate_volume.assert_called_with(export_mnt_base)
@mock.patch.object(quobyte, 'validate_volume')
@mock.patch.object(quobyte, 'umount_volume')
@mock.patch.object(libvirt_utils, 'is_mounted', return_value=True)
def test_libvirt_quobyte_driver_umount(self, mock_is_mounted,
mock_umount_volume,
mock_validate_volume):
mnt_base = '/mnt'
self.flags(quobyte_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtQuobyteVolumeDriver(self.fake_conn)
export_string = 'quobyte://192.168.1.1/volume-00001'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
file_path = os.path.join(export_mnt_base, self.name)
connection_info = {'data': {'export': export_string,
'name': self.name}}
libvirt_driver.connect_volume(connection_info, self.disk_info)
conf = libvirt_driver.get_config(connection_info, self.disk_info)
tree = conf.format_dom()
self._assertFileTypeEquals(tree, file_path)
libvirt_driver.disconnect_volume(connection_info, "vde")
mock_validate_volume.assert_called_once_with(export_mnt_base)
mock_umount_volume.assert_called_once_with(export_mnt_base)
@mock.patch.object(quobyte, 'validate_volume')
@mock.patch.object(quobyte, 'umount_volume')
def test_libvirt_quobyte_driver_already_mounted(self,
mock_umount_volume,
mock_validate_volume
):
mnt_base = '/mnt'
self.flags(quobyte_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtQuobyteVolumeDriver(self.fake_conn)
export_string = 'quobyte://192.168.1.1/volume-00001'
quobyte_volume = '192.168.1.1/volume-00001'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
file_path = os.path.join(export_mnt_base, self.name)
connection_info = {'data': {'export': export_string,
'name': self.name}}
libvirt_driver.connect_volume(connection_info, self.disk_info)
conf = libvirt_driver.get_config(connection_info, self.disk_info)
tree = conf.format_dom()
self._assertFileTypeEquals(tree, file_path)
libvirt_driver.disconnect_volume(connection_info, "vde")
expected_commands = [
('findmnt', '--target', export_mnt_base,
'--source', "quobyte@" + quobyte_volume),
('findmnt', '--target', export_mnt_base,
'--source', "quobyte@" + quobyte_volume),
]
self.assertEqual(expected_commands, self.executes)
mock_umount_volume.assert_called_once_with(export_mnt_base)
mock_validate_volume.assert_called_once_with(export_mnt_base)
@mock.patch.object(quobyte, 'validate_volume')
@mock.patch.object(quobyte, 'mount_volume')
@mock.patch.object(libvirt_utils, 'is_mounted', return_value=False)
def test_libvirt_quobyte_driver_qcow2(self, mock_is_mounted,
mock_mount_volume,
mock_validate_volume
):
mnt_base = '/mnt'
self.flags(quobyte_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtQuobyteVolumeDriver(self.fake_conn)
export_string = 'quobyte://192.168.1.1/volume-00001'
name = 'volume-00001'
image_format = 'qcow2'
quobyte_volume = '192.168.1.1/volume-00001'
connection_info = {'data': {'export': export_string,
'name': name,
'format': image_format}}
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(quobyte_volume))
libvirt_driver.connect_volume(connection_info, self.disk_info)
conf = libvirt_driver.get_config(connection_info, self.disk_info)
tree = conf.format_dom()
self.assertEqual(tree.get('type'), 'file')
self.assertEqual(tree.find('./driver').get('type'), 'qcow2')
(mock_mount_volume.
assert_called_once_with('192.168.1.1/volume-00001',
export_mnt_base,
mock.ANY))
mock_validate_volume.assert_called_with(export_mnt_base)
libvirt_driver.disconnect_volume(connection_info, "vde")
def test_libvirt_quobyte_driver_mount_non_quobyte_volume(self):
mnt_base = '/mnt'
self.flags(quobyte_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtQuobyteVolumeDriver(self.fake_conn)
export_string = 'quobyte://192.168.1.1/volume-00001'
connection_info = {'data': {'export': export_string,
'name': self.name}}
def exe_side_effect(*cmd, **kwargs):
if cmd == mock.ANY:
raise exception.NovaException()
with mock.patch.object(quobyte,
'validate_volume') as mock_execute:
mock_execute.side_effect = exe_side_effect
self.assertRaises(exception.NovaException,
libvirt_driver.connect_volume,
connection_info,
self.disk_info)
def test_libvirt_quobyte_driver_normalize_url_with_protocol(self):
mnt_base = '/mnt'
self.flags(quobyte_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtQuobyteVolumeDriver(self.fake_conn)
export_string = 'quobyte://192.168.1.1/volume-00001'
self.assertEqual(libvirt_driver._normalize_url(export_string),
"192.168.1.1/volume-00001")
def test_libvirt_quobyte_driver_normalize_url_without_protocol(self):
mnt_base = '/mnt'
self.flags(quobyte_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtQuobyteVolumeDriver(self.fake_conn)
export_string = '192.168.1.1/volume-00001'
self.assertEqual(libvirt_driver._normalize_url(export_string),
"192.168.1.1/volume-00001")

View File

@ -181,6 +181,8 @@ libvirt_opts = [
'nova.virt.libvirt.volume.LibvirtScalityVolumeDriver',
'gpfs='
'nova.virt.libvirt.volume.LibvirtGPFSVolumeDriver',
'quobyte='
'nova.virt.libvirt.volume.LibvirtQuobyteVolumeDriver',
],
help='DEPRECATED. Libvirt handlers for remote volumes. '
'This option is deprecated and will be removed in the '

View File

@ -0,0 +1,80 @@
# Copyright (c) 2015 Quobyte Inc.
# All Rights Reserved.
#
# 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 os
from oslo_concurrency import processutils
from nova import exception as nova_exception
from nova.i18n import _
from nova.i18n import _LE
from nova.i18n import _LI
from nova.openstack.common import fileutils
from nova.openstack.common import log as logging
from nova import utils
LOG = logging.getLogger(__name__)
SOURCE_PROTOCOL = 'quobyte'
SOURCE_TYPE = 'file'
DRIVER_CACHE = 'none'
DRIVER_IO = 'native'
def mount_volume(volume, mnt_base, configfile=None):
"""Wraps execute calls for mounting a Quobyte volume"""
fileutils.ensure_tree(mnt_base)
command = ['mount.quobyte', volume, mnt_base]
if configfile:
command.extend(['-c', configfile])
LOG.debug('Mounting volume %s at mount point %s ...',
volume,
mnt_base)
# Run mount command but do not fail on already mounted exit code
utils.execute(*command, check_exit_code=[0, 4])
LOG.info(_LI('Mounted volume: %s'), volume)
def umount_volume(mnt_base):
"""Wraps execute calls for unmouting a Quobyte volume"""
try:
utils.execute('umount.quobyte', mnt_base)
except processutils.ProcessExecutionError as exc:
if 'Device or resource busy' in exc.message:
LOG.error(_LE("The Quobyte volume at %s is still in use."),
mnt_base)
else:
LOG.exception(_LE("Couldn't unmount the Quobyte Volume at %s"),
mnt_base)
def validate_volume(mnt_base):
"""Wraps execute calls for checking validity of a Quobyte volume"""
command = ['getfattr', "-n", "quobyte.info", mnt_base]
try:
utils.execute(*command)
except processutils.ProcessExecutionError as exc:
msg = (_("The mount %(mount_path)s is not a valid"
" Quobyte volume. Error: %(exc)s")
% {'mount_path': mnt_base, 'exc': exc})
raise nova_exception.NovaException(msg)
if not os.access(mnt_base, os.W_OK | os.X_OK):
msg = (_LE("Volume is not writable. Please broaden the file"
" permissions. Mount: %s") % mnt_base)
raise nova_exception.NovaException(msg)

View File

@ -16,6 +16,7 @@
"""Volume drivers for libvirt."""
import errno
import glob
import os
import re
@ -31,6 +32,7 @@ import six.moves.urllib.parse as urlparse
from nova import exception
from nova.i18n import _
from nova.i18n import _LE
from nova.i18n import _LI
from nova.i18n import _LW
from nova.openstack.common import log as logging
from nova.openstack.common import loopingcall
@ -38,6 +40,7 @@ from nova import paths
from nova.storage import linuxscsi
from nova import utils
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import quobyte
from nova.virt.libvirt import remotefs
from nova.virt.libvirt import utils as libvirt_utils
@ -93,6 +96,12 @@ volume_opts = [
default=[],
help='Protocols listed here will be accessed directly '
'from QEMU. Currently supported protocols: [gluster]'),
cfg.StrOpt('quobyte_mount_point_base',
default=paths.state_path_def('mnt'),
help='Directory where the Quobyte volume is mounted on the '
'compute node'),
cfg.StrOpt('quobyte_client_cfg',
help='Path to a Quobyte Client configuration file.'),
cfg.StrOpt('iscsi_transport',
default=None,
help='The iSCSI transport to use to connect to target in case '
@ -1351,3 +1360,82 @@ class LibvirtGPFSVolumeDriver(LibvirtBaseVolumeDriver):
conf.source_type = "file"
conf.source_path = connection_info['data']['device_path']
return conf
class LibvirtQuobyteVolumeDriver(LibvirtBaseVolumeDriver):
"""Class implements libvirt part of volume driver for Quobyte."""
def __init__(self, connection):
"""Create back-end to Quobyte."""
super(LibvirtQuobyteVolumeDriver,
self).__init__(connection, is_block_dev=False)
def get_config(self, connection_info, disk_info):
conf = super(LibvirtQuobyteVolumeDriver,
self).get_config(connection_info, disk_info)
data = connection_info['data']
conf.source_protocol = quobyte.SOURCE_PROTOCOL
conf.source_type = quobyte.SOURCE_TYPE
conf.driver_cache = quobyte.DRIVER_CACHE
conf.driver_io = quobyte.DRIVER_IO
conf.driver_format = data.get('format', 'raw')
quobyte_volume = self._normalize_url(data['export'])
path = os.path.join(self._get_mount_point_for_share(quobyte_volume),
data['name'])
conf.source_path = path
return conf
@utils.synchronized('connect_volume')
def connect_volume(self, connection_info, disk_info):
"""Connect the volume."""
data = connection_info['data']
quobyte_volume = self._normalize_url(data['export'])
mount_path = self._get_mount_point_for_share(quobyte_volume)
mounted = libvirt_utils.is_mounted(mount_path,
quobyte.SOURCE_PROTOCOL
+ '@' + quobyte_volume)
if mounted:
try:
os.stat(mount_path)
except OSError as exc:
if exc.errno == errno.ENOTCONN:
mounted = False
LOG.info(_LI('Fixing previous mount %s which was not'
' unmounted correctly.'), mount_path)
quobyte.umount_volume(mount_path)
if not mounted:
quobyte.mount_volume(quobyte_volume,
mount_path,
CONF.libvirt.quobyte_client_cfg)
quobyte.validate_volume(mount_path)
@utils.synchronized('connect_volume')
def disconnect_volume(self, connection_info, disk_dev):
"""Disconnect the volume."""
quobyte_volume = self._normalize_url(connection_info['data']['export'])
mount_path = self._get_mount_point_for_share(quobyte_volume)
if libvirt_utils.is_mounted(mount_path, 'quobyte@' + quobyte_volume):
quobyte.umount_volume(mount_path)
else:
LOG.info(_LI("Trying to disconnected unmounted volume at %s"),
mount_path)
def _normalize_url(self, export):
protocol = quobyte.SOURCE_PROTOCOL + "://"
if export.startswith(protocol):
export = export[len(protocol):]
return export
def _get_mount_point_for_share(self, quobyte_volume):
"""Return mount point for Quobyte volume.
:param quobyte_volume: Example: storage-host/openstack-volumes
"""
return os.path.join(CONF.libvirt.quobyte_mount_point_base,
utils.get_hash_str(quobyte_volume))