Add Windows SMBFS connector

This patch adds a Windows SMBFS connector. Also, a Windows
RemoteFS class is added, being used by this connector, having
a similar interface with the Linux RemoteFS client class.

The patch using Windows os-brick connectors in the Hyper-V
Nova driver: https://review.openstack.org/#/c/273504/

The Windows connector factory function has been removed as it's not
needed anymore.

Change-Id: I0c753a667d58391da7a903d11ab1f4729e68461a
Implements: blueprint os-brick-windows-support
This commit is contained in:
Lucian Petrut 2016-01-26 14:50:18 +02:00
parent c5e3d8affb
commit 4045300fa9
9 changed files with 500 additions and 44 deletions

View File

@ -44,6 +44,7 @@ FIBRE_CHANNEL = "FIBRE_CHANNEL"
AOE = "AOE" AOE = "AOE"
DRBD = "DRBD" DRBD = "DRBD"
NFS = "NFS" NFS = "NFS"
SMBFS = 'SMBFS'
GLUSTERFS = "GLUSTERFS" GLUSTERFS = "GLUSTERFS"
LOCAL = "LOCAL" LOCAL = "LOCAL"
HUAWEISDSHYPERVISOR = "HUAWEISDSHYPERVISOR" HUAWEISDSHYPERVISOR = "HUAWEISDSHYPERVISOR"

View File

@ -92,6 +92,7 @@ connector_list = [
'os_brick.initiator.connectors.disco.DISCOConnector', 'os_brick.initiator.connectors.disco.DISCOConnector',
'os_brick.initiator.windows.base.BaseWindowsConnector', 'os_brick.initiator.windows.base.BaseWindowsConnector',
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector', 'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
] ]
# Mappings used to determine who to contruct in the factory # Mappings used to determine who to contruct in the factory
@ -146,6 +147,8 @@ _connector_mapping_linux_s390x = {
_connector_mapping_windows = { _connector_mapping_windows = {
initiator.ISCSI: initiator.ISCSI:
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector', 'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
initiator.SMBFS:
'os_brick.initiator.windows.smbfs.WindowsSMBFSConnector',
} }

View File

@ -1,43 +0,0 @@
# Copyright 2016 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_log import log as logging
from oslo_utils import importutils
from os_brick.i18n import _
LOG = logging.getLogger(__name__)
# TODO(lpetrut): once we move the protocol name constants to a
# separate module, use that instead.
_connector_dict = {
'ISCSI':
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
}
def factory(protocol, *args, **kwargs):
LOG.debug("Retrieving connector for protocol: %s.", protocol)
connector = _connector_dict.get(protocol.upper())
if not connector:
msg = (_("Invalid InitiatorConnector protocol "
"specified %(protocol)s") %
dict(protocol=protocol))
raise ValueError(msg)
conn_cls = importutils.import_class(connector)
return conn_cls(*args, **kwargs)

View File

@ -0,0 +1,94 @@
# Copyright 2016 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 os
from os_win import utilsfactory
from os_brick.initiator.windows import base as win_conn_base
from os_brick.remotefs import windows_remotefs as remotefs
from os_brick import utils
class WindowsSMBFSConnector(win_conn_base.BaseWindowsConnector):
def __init__(self, *args, **kwargs):
super(WindowsSMBFSConnector, self).__init__(*args, **kwargs)
# If this flag is set, we use the local paths in case of local
# shares. This is in fact mandatory in some cases, for example
# for the Hyper-C scenario.
self._local_path_for_loopback = kwargs.get('local_path_for_loopback',
False)
self._remotefsclient = remotefs.WindowsRemoteFsClient(
mount_type='smbfs',
*args, **kwargs)
self._smbutils = utilsfactory.get_smbutils()
@staticmethod
def get_connector_properties(*args, **kwargs):
# No connector properties updates in this case.
return {}
@utils.trace
def connect_volume(self, connection_properties):
self.ensure_share_mounted(connection_properties)
disk_path = self._get_disk_path(connection_properties)
device_info = {'type': 'file',
'path': disk_path}
return device_info
@utils.trace
def disconnect_volume(self, connection_properties):
export_path = self._get_export_path(connection_properties)
self._remotefsclient.unmount(export_path)
def _get_export_path(self, connection_properties):
return connection_properties['export'].replace('/', '\\')
def _get_disk_path(self, connection_properties):
# This is expected to be the share address, as an UNC path.
export_path = self._get_export_path(connection_properties)
mount_base = self._remotefsclient.get_mount_base()
use_local_path = (self._local_path_for_loopback and
self._smbutils.is_local_share(export_path))
disk_dir = export_path
if mount_base:
# This will be a symlink pointing to either the share
# path directly or to the local share path, if requested
# and available.
disk_dir = self._remotefsclient.get_mount_point(
export_path)
elif use_local_path:
share_name = self._remotefsclient.get_share_name(export_path)
disk_dir = self._remotefsclient.get_local_share_path(share_name)
disk_name = connection_properties['name']
disk_path = os.path.join(disk_dir, disk_name)
return disk_path
def get_search_path(self):
return self._remotefsclient.get_mount_base()
@utils.trace
def get_volume_paths(self, connection_properties):
return [self._get_disk_path(connection_properties)]
def ensure_share_mounted(self, connection_properties):
export_path = self._get_export_path(connection_properties)
mount_options = connection_properties.get('options')
self._remotefsclient.mount(export_path, mount_options)
def extend_volume(self, connection_properties):
raise NotImplementedError

View File

@ -0,0 +1,122 @@
# Copyright 2016 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.
"""Windows remote filesystem client utilities."""
import os
import re
from oslo_log import log as logging
from os_win import utilsfactory
from os_brick import exception
from os_brick.i18n import _, _LI
from os_brick.remotefs import remotefs
LOG = logging.getLogger(__name__)
class WindowsRemoteFsClient(remotefs.RemoteFsClient):
_username_regex = re.compile(r'user(?:name)?=([^, ]+)')
_password_regex = re.compile(r'pass(?:word)?=([^, ]+)')
_loopback_share_map = {}
def __init__(self, mount_type, root_helper=None,
execute=None, *args, **kwargs):
mount_type_to_option_prefix = {
'cifs': 'smbfs',
'smbfs': 'smbfs',
}
self._local_path_for_loopback = kwargs.get('local_path_for_loopback',
False)
if mount_type not in mount_type_to_option_prefix:
raise exception.ProtocolNotSupported(protocol=mount_type)
self._mount_type = mount_type
option_prefix = mount_type_to_option_prefix[mount_type]
self._mount_base = kwargs.get(option_prefix + '_mount_point_base')
self._mount_options = kwargs.get(option_prefix + '_mount_options')
self._smbutils = utilsfactory.get_smbutils()
self._pathutils = utilsfactory.get_pathutils()
def get_local_share_path(self, share, expect_existing=True):
local_share_path = self._smbutils.get_smb_share_path(share)
if not local_share_path and expect_existing:
err_msg = _("Could not find the local "
"share path for %(share)s.")
raise exception.VolumePathsNotFound(err_msg % dict(share=share))
return local_share_path
def get_share_name(self, share):
return share.replace('/', '\\').lstrip('\\').split('\\', 1)[1]
def mount(self, share, flags=None):
share = share.replace('/', '\\')
use_local_path = (self._local_path_for_loopback and
self._smbutils.is_local_share(share))
if use_local_path:
LOG.info(_LI("Skipping mounting local share %(share_path)s."),
dict(share_path=share))
else:
mount_options = " ".join(
[self._mount_options or '', flags or ''])
username, password = self._parse_credentials(mount_options)
if not self._smbutils.check_smb_mapping(
share):
self._smbutils.mount_smb_share(share,
username=username,
password=password)
if self._mount_base:
share_name = self.get_share_name(share)
symlink_dest = (share if not use_local_path
else self.get_local_share_path(share_name))
self._create_mount_point(symlink_dest)
def unmount(self, share):
self._smbutils.unmount_smb_share(share.replace('/', '\\'))
def _create_mount_point(self, share):
mnt_point = self.get_mount_point(share)
if not os.path.isdir(self._mount_base):
os.makedirs(self._mount_base)
if os.path.exists(mnt_point):
if not self._pathutils.is_symlink(mnt_point):
raise exception.BrickException(_("Link path already exists "
"and it's not a symlink"))
else:
self._pathutils.create_sym_link(mnt_point, share)
def _parse_credentials(self, opts_str):
if not opts_str:
return None, None
match = self._username_regex.findall(opts_str)
username = match[0] if match and match[0] != 'guest' else None
match = self._password_regex.findall(opts_str)
password = match[0] if match else None
return username, password

View File

@ -0,0 +1,136 @@
# Copyright 2016 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 ddt
import mock
from os_brick import exception
from os_brick.remotefs import windows_remotefs
from os_brick.tests import base
@ddt.ddt
class WindowsRemotefsClientTestCase(base.TestCase):
_FAKE_SHARE_NAME = 'fake_share'
_FAKE_SHARE_SERVER = 'fake_share_server'
_FAKE_SHARE = '\\\\%s\\%s' % (_FAKE_SHARE_SERVER,
_FAKE_SHARE_NAME)
@mock.patch.object(windows_remotefs, 'utilsfactory')
def setUp(self, mock_utilsfactory):
super(WindowsRemotefsClientTestCase, self).setUp()
self._remotefs = windows_remotefs.WindowsRemoteFsClient(
mount_type='smbfs')
self._remotefs._mount_base = mock.sentinel.mount_base
self._smbutils = self._remotefs._smbutils
self._pathutils = self._remotefs._pathutils
@ddt.data({},
{'expect_existing': False},
{'local_path': mock.sentinel.local_path})
@ddt.unpack
def test_get_local_share_path(self, expect_existing=True,
local_path=None):
self._smbutils.get_smb_share_path.return_value = local_path
if not local_path and expect_existing:
self.assertRaises(
exception.VolumePathsNotFound,
self._remotefs.get_local_share_path,
mock.sentinel.share_name,
expect_existing=expect_existing)
else:
share_path = self._remotefs.get_local_share_path(
mock.sentinel.share_name,
expect_existing=expect_existing)
self.assertEqual(local_path, share_path)
def test_get_share_name(self):
resulted_name = self._remotefs.get_share_name(self._FAKE_SHARE)
self.assertEqual(self._FAKE_SHARE_NAME, resulted_name)
@ddt.data(True, False)
@mock.patch.object(windows_remotefs.WindowsRemoteFsClient,
'_create_mount_point')
@mock.patch.object(windows_remotefs.WindowsRemoteFsClient,
'get_local_share_path')
def test_mount(self, is_local_share,
mock_get_local_share_path,
mock_create_mount_point):
flags = '-o pass=password'
self._remotefs._mount_options = '-o user=username,randomopt'
self._remotefs._local_path_for_loopback = True
self._smbutils.check_smb_mapping.return_value = False
self._smbutils.is_local_share.return_value = is_local_share
self._remotefs.mount(self._FAKE_SHARE, flags)
if is_local_share:
expected_link_dest = mock_get_local_share_path.return_value
self.assertFalse(self._smbutils.check_smb_mapping.called)
self.assertFalse(self._smbutils.mount_smb_share.called)
mock_get_local_share_path.assert_called_once_with(
self._FAKE_SHARE_NAME)
else:
expected_link_dest = self._FAKE_SHARE
self._smbutils.check_smb_mapping.assert_called_once_with(
self._FAKE_SHARE)
self._smbutils.mount_smb_share.assert_called_once_with(
self._FAKE_SHARE,
username='username',
password='password')
self.assertFalse(mock_get_local_share_path.called)
mock_create_mount_point.assert_called_once_with(expected_link_dest)
def test_unmount(self):
self._remotefs.unmount(self._FAKE_SHARE)
self._smbutils.unmount_smb_share.assert_called_once_with(
self._FAKE_SHARE)
@ddt.data({},
{'path_exists': True, 'is_symlink': True},
{'path_exists': True})
@mock.patch.object(windows_remotefs.WindowsRemoteFsClient,
'get_mount_point')
@mock.patch.object(windows_remotefs, 'os')
@ddt.unpack
def test_create_mount_point(self, mock_os, mock_get_mount_point,
path_exists=False, is_symlink=False):
mock_os.path.exists.return_value = path_exists
mock_os.isdir.return_value = False
self._pathutils.is_symlink.return_value = is_symlink
if path_exists and not is_symlink:
self.assertRaises(exception.BrickException,
self._remotefs._create_mount_point,
mock.sentinel.share)
else:
self._remotefs._create_mount_point(mock.sentinel.share)
mock_get_mount_point.assert_called_once_with(mock.sentinel.share)
mock_os.path.isdir.assert_called_once_with(mock.sentinel.mount_base)
if path_exists:
self._pathutils.is_symlink.assert_called_once_with(
mock_get_mount_point.return_value)
else:
self._pathutils.create_sym_link.assert_called_once_with(
mock_get_mount_point.return_value,
mock.sentinel.share)

View File

@ -19,13 +19,16 @@ import mock
from os_brick import initiator from os_brick import initiator
from os_brick.initiator import connector from os_brick.initiator import connector
from os_brick.initiator.windows import iscsi from os_brick.initiator.windows import iscsi
from os_brick.initiator.windows import smbfs
from os_brick.tests.windows import test_base from os_brick.tests.windows import test_base
@ddt.ddt @ddt.ddt
class WindowsConnectorFactoryTestCase(test_base.WindowsConnectorTestBase): class WindowsConnectorFactoryTestCase(test_base.WindowsConnectorTestBase):
@ddt.data({'proto': initiator.ISCSI, @ddt.data({'proto': initiator.ISCSI,
'expected_cls': iscsi.WindowsISCSIConnector}) 'expected_cls': iscsi.WindowsISCSIConnector},
{'proto': initiator.SMBFS,
'expected_cls': smbfs.WindowsSMBFSConnector})
@ddt.unpack @ddt.unpack
@mock.patch('sys.platform', 'win32') @mock.patch('sys.platform', 'win32')
def test_factory(self, proto, expected_cls): def test_factory(self, proto, expected_cls):

View File

@ -0,0 +1,137 @@
# Copyright 2016 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 os
import ddt
import mock
from os_brick.initiator.windows import smbfs
from os_brick.remotefs import windows_remotefs
from os_brick.tests.windows import test_base
@ddt.ddt
class WindowsSMBFSConnectorTestCase(test_base.WindowsConnectorTestBase):
@mock.patch.object(windows_remotefs, 'WindowsRemoteFsClient')
def setUp(self, mock_remotefs_cls):
super(WindowsSMBFSConnectorTestCase, self).setUp()
self._connector = smbfs.WindowsSMBFSConnector()
self._remotefs = mock_remotefs_cls.return_value
@mock.patch.object(smbfs.WindowsSMBFSConnector, '_get_disk_path')
@mock.patch.object(smbfs.WindowsSMBFSConnector, 'ensure_share_mounted')
def test_connect_volume(self, mock_ensure_mounted,
mock_get_disk_path):
device_info = self._connector.connect_volume(mock.sentinel.conn_props)
expected_info = dict(type='file',
path=mock_get_disk_path.return_value)
self.assertEqual(expected_info, device_info)
mock_ensure_mounted.assert_called_once_with(mock.sentinel.conn_props)
mock_get_disk_path.assert_called_once_with(mock.sentinel.conn_props)
@mock.patch.object(smbfs.WindowsSMBFSConnector, '_get_export_path')
def test_disconnect_volume(self, mock_get_export_path):
self._connector.disconnect_volume(mock.sentinel.conn_props)
self._remotefs.unmount.assert_called_once_with(
mock_get_export_path.return_value)
mock_get_export_path.assert_called_once_with(mock.sentinel.conn_props)
def test_get_export_path(self):
fake_export = '//ip/share'
fake_conn_props = dict(export=fake_export)
expected_export = fake_export.replace('/', '\\')
export_path = self._connector._get_export_path(fake_conn_props)
self.assertEqual(expected_export, export_path)
@ddt.data({},
{'mount_base': mock.sentinel.mount_base},
{'is_local_share': True},
{'is_local_share': True,
'local_path_for_loopbk': True})
@ddt.unpack
def test_get_disk_path(self, mount_base=None,
local_path_for_loopbk=False,
is_local_share=False):
fake_mount_point = r'C:\\fake_mount_point'
fake_share_name = 'fake_share'
fake_local_share_path = 'C:\\%s' % fake_share_name
fake_export_path = '\\\\host\\%s' % fake_share_name
fake_disk_name = 'fake_disk.vhdx'
fake_conn_props = dict(name=fake_disk_name,
export=fake_export_path)
self._remotefs.get_mount_base.return_value = mount_base
self._remotefs.get_mount_point.return_value = fake_mount_point
self._remotefs.get_local_share_path.return_value = (
fake_local_share_path)
self._remotefs.get_share_name.return_value = fake_share_name
self._connector._local_path_for_loopback = local_path_for_loopbk
self._connector._smbutils.is_local_share.return_value = is_local_share
expecting_local = local_path_for_loopbk and is_local_share
if mount_base:
expected_export_path = fake_mount_point
elif expecting_local:
# In this case, we expect the local share export path to be
# used directly.
expected_export_path = fake_local_share_path
else:
expected_export_path = fake_export_path
expected_disk_path = os.path.join(expected_export_path,
fake_disk_name)
disk_path = self._connector._get_disk_path(fake_conn_props)
self.assertEqual(expected_disk_path, disk_path)
if mount_base:
self._remotefs.get_mount_point.assert_called_once_with(
fake_export_path)
elif expecting_local:
self._connector._smbutils.is_local_share.assert_called_once_with(
fake_export_path)
self._remotefs.get_share_name.assert_called_once_with(
fake_export_path)
self._remotefs.get_local_share_path.assert_called_once_with(
fake_share_name)
def test_get_search_path(self):
search_path = self._connector.get_search_path()
self.assertEqual(search_path,
self._remotefs.get_mount_base.return_value)
@mock.patch.object(smbfs.WindowsSMBFSConnector, '_get_disk_path')
def test_volume_paths(self, mock_get_disk_path):
expected_paths = [mock_get_disk_path.return_value]
volume_paths = self._connector.get_volume_paths(
mock.sentinel.conn_props)
self.assertEqual(expected_paths, volume_paths)
mock_get_disk_path.assert_called_once_with(
mock.sentinel.conn_props)
@mock.patch.object(smbfs.WindowsSMBFSConnector, '_get_export_path')
def test_ensure_share_mounted(self, mock_get_export_path):
fake_conn_props = dict(options=mock.sentinel.mount_opts)
self._connector.ensure_share_mounted(fake_conn_props)
self._remotefs.mount.assert_called_once_with(
mock_get_export_path.return_value,
mock.sentinel.mount_opts)

View File

@ -0,0 +1,3 @@
---
features:
- Add Windows SMBFS connector support.