Merge "Libvirt: SMB volume driver"

This commit is contained in:
Jenkins 2014-12-09 17:57:57 +00:00 committed by Gerrit Code Review
commit 512a7f2a30
5 changed files with 283 additions and 0 deletions

View File

@ -0,0 +1,60 @@
# Copyright 2014 Cloudbase Solutions Srl
# 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 mock
from oslo.concurrency import processutils
from nova import test
from nova import utils
from nova.virt.libvirt import remotefs
class RemoteFSTestCase(test.NoDBTestCase):
"""Remote filesystem operations test case."""
@mock.patch.object(utils, 'execute')
def _test_mount_share(self, mock_execute, already_mounted=False):
if already_mounted:
err_msg = 'Device or resource busy'
mock_execute.side_effect = [
None, processutils.ProcessExecutionError(err_msg)]
remotefs.mount_share(
mock.sentinel.mount_path, mock.sentinel.export_path,
mock.sentinel.export_type,
options=[mock.sentinel.mount_options])
mock_execute.assert_any_call('mkdir', '-p',
mock.sentinel.mount_path)
mock_execute.assert_any_call('mount', '-t', mock.sentinel.export_type,
mock.sentinel.mount_options,
mock.sentinel.export_path,
mock.sentinel.mount_path,
run_as_root=True)
def test_mount_new_share(self):
self._test_mount_share()
def test_mount_already_mounted_share(self):
self._test_mount_share(already_mounted=True)
@mock.patch.object(utils, 'execute')
def test_unmount_share(self, mock_execute):
remotefs.unmount_share(
mock.sentinel.mount_path, mock.sentinel.export_path)
mock_execute.assert_any_call('umount', mock.sentinel.mount_path,
run_as_root=True, attempts=3,
delay_on_retry=True)

View File

@ -1267,3 +1267,86 @@ Setting up iSCSI targets: unused
conf = driver.get_config(TEST_CONN_INFO, self.disk_info)
tree = conf.format_dom()
self._assertFileTypeEquals(tree, TEST_VOLPATH)
@mock.patch.object(libvirt_utils, 'is_mounted')
def test_libvirt_smbfs_driver(self, mock_is_mounted):
mnt_base = '/mnt'
self.flags(smbfs_mount_point_base=mnt_base, group='libvirt')
mock_is_mounted.return_value = False
libvirt_driver = volume.LibvirtSMBFSVolumeDriver(self.fake_conn)
export_string = '//192.168.1.1/volumes'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(export_string))
connection_info = {'data': {'export': export_string,
'name': self.name}}
libvirt_driver.connect_volume(connection_info, self.disk_info)
libvirt_driver.disconnect_volume(connection_info, "vde")
expected_commands = [
('mkdir', '-p', export_mnt_base),
('mount', '-t', 'cifs', '-o', 'username=guest',
export_string, export_mnt_base),
('umount', export_mnt_base)]
self.assertEqual(expected_commands, self.executes)
def test_libvirt_smbfs_driver_already_mounted(self):
mnt_base = '/mnt'
self.flags(smbfs_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtSMBFSVolumeDriver(self.fake_conn)
export_string = '//192.168.1.1/volumes'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(export_string))
connection_info = {'data': {'export': export_string,
'name': self.name}}
libvirt_driver.connect_volume(connection_info, self.disk_info)
libvirt_driver.disconnect_volume(connection_info, "vde")
expected_commands = [
('findmnt', '--target', export_mnt_base,
'--source', export_string),
('umount', export_mnt_base)]
self.assertEqual(expected_commands, self.executes)
def test_libvirt_smbfs_driver_get_config(self):
mnt_base = '/mnt'
self.flags(smbfs_mount_point_base=mnt_base, group='libvirt')
libvirt_driver = volume.LibvirtSMBFSVolumeDriver(self.fake_conn)
export_string = '//192.168.1.1/volumes'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(export_string))
file_path = os.path.join(export_mnt_base, self.name)
connection_info = {'data': {'export': export_string,
'name': self.name,
'device_path': file_path}}
conf = libvirt_driver.get_config(connection_info, self.disk_info)
tree = conf.format_dom()
self._assertFileTypeEquals(tree, file_path)
@mock.patch.object(libvirt_utils, 'is_mounted')
def test_libvirt_smbfs_driver_with_opts(self, mock_is_mounted):
mnt_base = '/mnt'
self.flags(smbfs_mount_point_base=mnt_base, group='libvirt')
mock_is_mounted.return_value = False
libvirt_driver = volume.LibvirtSMBFSVolumeDriver(self.fake_conn)
export_string = '//192.168.1.1/volumes'
options = '-o user=guest,uid=107,gid=105'
export_mnt_base = os.path.join(mnt_base,
utils.get_hash_str(export_string))
connection_info = {'data': {'export': export_string,
'name': self.name,
'options': options}}
libvirt_driver.connect_volume(connection_info, self.disk_info)
libvirt_driver.disconnect_volume(connection_info, "vde")
expected_commands = [
('mkdir', '-p', export_mnt_base),
('mount', '-t', 'cifs', '-o', 'user=guest,uid=107,gid=105',
export_string, export_mnt_base),
('umount', export_mnt_base)]
self.assertEqual(expected_commands, self.executes)

View File

@ -171,6 +171,7 @@ libvirt_opts = [
'rbd=nova.virt.libvirt.volume.LibvirtNetVolumeDriver',
'sheepdog=nova.virt.libvirt.volume.LibvirtNetVolumeDriver',
'nfs=nova.virt.libvirt.volume.LibvirtNFSVolumeDriver',
'smbfs=nova.virt.libvirt.volume.LibvirtSMBFSVolumeDriver',
'aoe=nova.virt.libvirt.volume.LibvirtAOEVolumeDriver',
'glusterfs='
'nova.virt.libvirt.volume.LibvirtGlusterfsVolumeDriver',

View File

@ -0,0 +1,64 @@
# Copyright 2014 Cloudbase Solutions Srl
# 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.
from oslo.concurrency import processutils
from nova.i18n import _LE, _LW
from nova.openstack.common import log as logging
from nova import utils
LOG = logging.getLogger(__name__)
def mount_share(mount_path, export_path,
export_type, options=None):
"""Mount a remote export to mount_path.
:param mount_path: place where the remote export will be mounted
:param export_path: path of the export to be mounted
:export_type: remote export type (e.g. cifs, nfs, etc.)
:options: A list containing mount options
"""
utils.execute('mkdir', '-p', mount_path)
mount_cmd = ['mount', '-t', export_type]
if options is not None:
mount_cmd.extend(options)
mount_cmd.extend([export_path, mount_path])
try:
utils.execute(*mount_cmd, run_as_root=True)
except processutils.ProcessExecutionError as exc:
if 'Device or resource busy' in exc.message:
LOG.warn(_LW("%s is already mounted"), export_path)
else:
raise
def unmount_share(mount_path, export_path):
"""Unmount a remote share.
:param mount_path: remote export mount point
:param export_path: path of the remote export to be unmounted
"""
try:
utils.execute('umount', mount_path, run_as_root=True,
attempts=3, delay_on_retry=True)
except processutils.ProcessExecutionError as exc:
if 'target is busy' in exc.message:
LOG.debug("The share %s is still in use.", export_path)
else:
LOG.exception(_LE("Couldn't unmount the share %s"),
export_path)

View File

@ -18,6 +18,7 @@
import glob
import os
import re
import time
import urllib2
@ -37,6 +38,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 remotefs
from nova.virt.libvirt import utils as libvirt_utils
LOG = logging.getLogger(__name__)
@ -60,6 +62,15 @@ volume_opts = [
cfg.StrOpt('nfs_mount_options',
help='Mount options passedf to the NFS client. See section '
'of the nfs man page for details'),
cfg.StrOpt('smbfs_mount_point_base',
default=paths.state_path_def('mnt'),
help='Directory where the SMBFS shares are mounted on the '
'compute node'),
cfg.StrOpt('smbfs_mount_options',
default='',
help='Mount options passed to the SMBFS client. See '
'mount.cifs man page for details. Note that the '
'libvirt-qemu uid and gid must be specified.'),
cfg.IntOpt('num_aoe_discover_tries',
default=3,
help='Number of times to rediscover AoE target to find volume'),
@ -766,6 +777,70 @@ class LibvirtNFSVolumeDriver(LibvirtBaseVolumeDriver):
raise
class LibvirtSMBFSVolumeDriver(LibvirtBaseVolumeDriver):
"""Class implements libvirt part of volume driver for SMBFS."""
def __init__(self, connection):
super(LibvirtSMBFSVolumeDriver,
self).__init__(connection, is_block_dev=False)
self.username_regex = re.compile(
r"(user(?:name)?)=(?:[^ ,]+\\)?([^ ,]+)")
def _get_device_path(self, connection_info):
smbfs_share = connection_info['data']['export']
mount_path = self._get_mount_path(smbfs_share)
volume_path = os.path.join(mount_path,
connection_info['data']['name'])
return volume_path
def _get_mount_path(self, smbfs_share):
mount_path = os.path.join(CONF.libvirt.smbfs_mount_point_base,
utils.get_hash_str(smbfs_share))
return mount_path
def get_config(self, connection_info, disk_info):
"""Returns xml for libvirt."""
conf = super(LibvirtSMBFSVolumeDriver,
self).get_config(connection_info, disk_info)
conf.source_type = 'file'
conf.driver_cache = 'writethrough'
conf.source_path = connection_info['data']['device_path']
conf.driver_format = connection_info['data'].get('format', 'raw')
return conf
def connect_volume(self, connection_info, disk_info):
"""Connect the volume."""
smbfs_share = connection_info['data']['export']
mount_path = self._get_mount_path(smbfs_share)
if not libvirt_utils.is_mounted(mount_path, smbfs_share):
mount_options = self._parse_mount_options(connection_info)
remotefs.mount_share(mount_path, smbfs_share,
export_type='cifs', options=mount_options)
device_path = self._get_device_path(connection_info)
connection_info['data']['device_path'] = device_path
def disconnect_volume(self, connection_info, disk_dev):
"""Disconnect the volume."""
smbfs_share = connection_info['data']['export']
mount_path = self._get_mount_path(smbfs_share)
remotefs.unmount_share(mount_path, smbfs_share)
def _parse_mount_options(self, connection_info):
mount_options = " ".join(
[connection_info['data'].get('options', ''),
CONF.libvirt.smbfs_mount_options])
if not self.username_regex.findall(mount_options):
mount_options = mount_options + ' -o username=guest'
else:
# Remove the Domain Name from user name
mount_options = self.username_regex.sub(r'\1=\2', mount_options)
return mount_options.strip(", ").split(' ')
class LibvirtAOEVolumeDriver(LibvirtBaseVolumeDriver):
"""Driver to attach AoE volumes to libvirt."""
def __init__(self, connection):