Merge "Libvirt: SMB volume driver"
This commit is contained in:
commit
512a7f2a30
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue