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:
parent
c5e3d8affb
commit
4045300fa9
@ -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"
|
||||||
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
|
94
os_brick/initiator/windows/smbfs.py
Normal file
94
os_brick/initiator/windows/smbfs.py
Normal 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
|
122
os_brick/remotefs/windows_remotefs.py
Normal file
122
os_brick/remotefs/windows_remotefs.py
Normal 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
|
136
os_brick/tests/remotefs/test_windows_remotefs.py
Normal file
136
os_brick/tests/remotefs/test_windows_remotefs.py
Normal 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)
|
@ -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):
|
||||||
|
137
os_brick/tests/windows/test_smbfs.py
Normal file
137
os_brick/tests/windows/test_smbfs.py
Normal 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)
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Add Windows SMBFS connector support.
|
Loading…
Reference in New Issue
Block a user