Merge "Add Windows iSCSI connector"
This commit is contained in:
commit
8bf860729e
@ -52,6 +52,7 @@ from os_brick.initiator import linuxfc
|
||||
from os_brick.initiator import linuxrbd
|
||||
from os_brick.initiator import linuxscsi
|
||||
from os_brick.initiator import linuxsheepdog
|
||||
from os_brick.initiator import windows as windows_connector
|
||||
from os_brick.remotefs import remotefs
|
||||
from os_brick.i18n import _, _LE, _LI, _LW
|
||||
|
||||
@ -68,6 +69,7 @@ PLATFORM_x86 = 'X86'
|
||||
PLATFORM_S390 = 'S390'
|
||||
OS_TYPE_ALL = 'ALL'
|
||||
OS_TYPE_LINUX = 'LINUX'
|
||||
OS_TYPE_WINDOWS = 'WIN'
|
||||
|
||||
S390X = "s390x"
|
||||
S390 = "s390"
|
||||
@ -104,6 +106,8 @@ connector_list = [
|
||||
'os_brick.initiator.connector.HGSTConnector',
|
||||
'os_brick.initiator.connector.ScaleIOConnector',
|
||||
'os_brick.initiator.connector.DISCOConnector',
|
||||
'os_brick.initiator.windows.base.BaseWindowsConnector',
|
||||
'os_brick.initiator.windows.iscsi.WindowsISCSIConnector',
|
||||
]
|
||||
|
||||
|
||||
@ -189,6 +193,13 @@ class InitiatorConnector(executor.Executor):
|
||||
*args, **kwargs):
|
||||
"""Build a Connector object based upon protocol and architecture."""
|
||||
|
||||
if sys.platform == 'win32':
|
||||
return windows_connector.factory(
|
||||
protocol,
|
||||
use_multipath=use_multipath,
|
||||
device_scan_attempts=device_scan_attempts,
|
||||
*args, **kwargs)
|
||||
|
||||
# We do this instead of assigning it in the definition
|
||||
# to help mocking for unit tests
|
||||
if arch is None:
|
||||
@ -549,7 +560,35 @@ class FakeConnector(BaseLinuxConnector):
|
||||
'/dev/disk/by-path/fake-volume-X']
|
||||
|
||||
|
||||
class ISCSIConnector(BaseLinuxConnector):
|
||||
class BaseISCSIConnector(InitiatorConnector):
|
||||
def _iterate_all_targets(self, connection_properties):
|
||||
for portal, iqn, lun in self._get_all_targets(connection_properties):
|
||||
props = copy.deepcopy(connection_properties)
|
||||
props['target_portal'] = portal
|
||||
props['target_iqn'] = iqn
|
||||
props['target_lun'] = lun
|
||||
for key in ('target_portals', 'target_iqns', 'target_luns'):
|
||||
props.pop(key, None)
|
||||
yield props
|
||||
|
||||
def _get_all_targets(self, connection_properties):
|
||||
if all([key in connection_properties for key in ('target_portals',
|
||||
'target_iqns',
|
||||
'target_luns')]):
|
||||
return zip(connection_properties['target_portals'],
|
||||
connection_properties['target_iqns'],
|
||||
connection_properties['target_luns'])
|
||||
|
||||
return [(connection_properties['target_portal'],
|
||||
connection_properties['target_iqn'],
|
||||
connection_properties.get('target_lun', 0))]
|
||||
|
||||
|
||||
class FakeBaseISCSIConnector(FakeConnector, BaseISCSIConnector):
|
||||
pass
|
||||
|
||||
|
||||
class ISCSIConnector(BaseLinuxConnector, BaseISCSIConnector):
|
||||
"""Connector class to attach/detach iSCSI volumes."""
|
||||
supported_transports = ['be2iscsi', 'bnx2i', 'cxgb3i', 'default',
|
||||
'cxgb4i', 'qla4xxx', 'ocs', 'iser']
|
||||
@ -795,28 +834,6 @@ class ISCSIConnector(BaseLinuxConnector):
|
||||
def _get_transport(self):
|
||||
return self.transport
|
||||
|
||||
def _iterate_all_targets(self, connection_properties):
|
||||
for portal, iqn, lun in self._get_all_targets(connection_properties):
|
||||
props = copy.deepcopy(connection_properties)
|
||||
props['target_portal'] = portal
|
||||
props['target_iqn'] = iqn
|
||||
props['target_lun'] = lun
|
||||
for key in ('target_portals', 'target_iqns', 'target_luns'):
|
||||
props.pop(key, None)
|
||||
yield props
|
||||
|
||||
def _get_all_targets(self, connection_properties):
|
||||
if all([key in connection_properties for key in ('target_portals',
|
||||
'target_iqns',
|
||||
'target_luns')]):
|
||||
return zip(connection_properties['target_portals'],
|
||||
connection_properties['target_iqns'],
|
||||
connection_properties['target_luns'])
|
||||
|
||||
return [(connection_properties['target_portal'],
|
||||
connection_properties['target_iqn'],
|
||||
connection_properties.get('target_lun', 0))]
|
||||
|
||||
def _discover_iscsi_portals(self, connection_properties):
|
||||
if all([key in connection_properties for key in ('target_portals',
|
||||
'target_iqns')]):
|
||||
|
43
os_brick/initiator/windows/__init__.py
Normal file
43
os_brick/initiator/windows/__init__.py
Normal file
@ -0,0 +1,43 @@
|
||||
# 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)
|
108
os_brick/initiator/windows/base.py
Normal file
108
os_brick/initiator/windows/base.py
Normal file
@ -0,0 +1,108 @@
|
||||
# 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 os_win import utilsfactory
|
||||
from oslo_log import log as logging
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.i18n import _, _LE
|
||||
from os_brick.initiator import connector
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseWindowsConnector(connector.InitiatorConnector):
|
||||
platform = connector.PLATFORM_ALL
|
||||
os_type = connector.OS_TYPE_WINDOWS
|
||||
|
||||
def __init__(self, root_helper=None, *args, **kwargs):
|
||||
super(BaseWindowsConnector, self).__init__(root_helper,
|
||||
*args, **kwargs)
|
||||
self._diskutils = utilsfactory.get_diskutils()
|
||||
|
||||
@staticmethod
|
||||
def check_multipath_support(enforce_multipath):
|
||||
hostutils = utilsfactory.get_hostutils()
|
||||
mpio_enabled = hostutils.check_server_feature(
|
||||
hostutils.FEATURE_MPIO)
|
||||
if not mpio_enabled:
|
||||
err_msg = _LE(
|
||||
"Using multipath connections for iSCSI and FC disks "
|
||||
"requires the Multipath IO Windows feature to be "
|
||||
"enabled. MPIO must be configured to claim such devices.")
|
||||
LOG.error(err_msg)
|
||||
if enforce_multipath:
|
||||
raise exception.BrickException(err_msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_connector_properties(*args, **kwargs):
|
||||
multipath = kwargs['multipath']
|
||||
enforce_multipath = kwargs['enforce_multipath']
|
||||
|
||||
props = {}
|
||||
props['multipath'] = (
|
||||
multipath and
|
||||
BaseWindowsConnector.check_multipath_support(enforce_multipath))
|
||||
return props
|
||||
|
||||
def _get_scsi_wwn(self, device_number):
|
||||
# NOTE(lpetrut): The Linux connectors use scsi_id to retrieve the
|
||||
# disk unique id, which prepends the identifier type to the unique id
|
||||
# retrieved from the page 83 SCSI inquiry data. We'll do the same
|
||||
# to remain consistent.
|
||||
disk_uid, uid_type = self._diskutils.get_disk_uid_and_uid_type(
|
||||
device_number)
|
||||
scsi_wwn = '%s%s' % (uid_type, disk_uid)
|
||||
return scsi_wwn
|
||||
|
||||
def check_valid_device(self, path, *args, **kwargs):
|
||||
try:
|
||||
with open(path, 'r') as dev:
|
||||
dev.read(1)
|
||||
except IOError:
|
||||
LOG.exception(
|
||||
_LE("Failed to access the device on the path "
|
||||
"%(path)s"), {"path": path})
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_all_available_volumes(self):
|
||||
# TODO(lpetrut): query for disks based on the protocol used.
|
||||
return []
|
||||
|
||||
def _check_device_paths(self, device_paths):
|
||||
if len(device_paths) > 1:
|
||||
err_msg = _("Multiple volume paths were found: %s. This can "
|
||||
"occur if multipath is used and MPIO is not "
|
||||
"properly configured, thus not claiming the device "
|
||||
"paths. This issue must be addressed urgently as "
|
||||
"it can lead to data corruption.")
|
||||
raise exception.BrickException(err_msg % device_paths)
|
||||
|
||||
def extend_volume(self, connection_properties):
|
||||
volume_paths = self.get_volume_paths(connection_properties)
|
||||
if not volume_paths:
|
||||
err_msg = _("Could not find the disk. Extend failed.")
|
||||
raise exception.NotFound(err_msg)
|
||||
|
||||
device_path = volume_paths[0]
|
||||
device_number = self._diskutils.get_device_number_from_device_name(
|
||||
device_path)
|
||||
self._diskutils.refresh_disk(device_number)
|
||||
|
||||
def get_search_path(self):
|
||||
return None
|
155
os_brick/initiator/windows/iscsi.py
Normal file
155
os_brick/initiator/windows/iscsi.py
Normal file
@ -0,0 +1,155 @@
|
||||
# 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 os_win import exceptions as os_win_exc
|
||||
from os_win import utilsfactory
|
||||
from oslo_log import log as logging
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.i18n import _, _LE, _LI, _LW
|
||||
from os_brick.initiator import connector
|
||||
from os_brick.initiator.windows import base as win_conn_base
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WindowsISCSIConnector(win_conn_base.BaseWindowsConnector,
|
||||
connector.BaseISCSIConnector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WindowsISCSIConnector, self).__init__(*args, **kwargs)
|
||||
self.use_multipath = kwargs.pop('use_multipath', False)
|
||||
self.initiator_list = kwargs.pop('initiator_list', [])
|
||||
|
||||
self._iscsi_utils = utilsfactory.get_iscsi_initiator_utils()
|
||||
|
||||
self.validate_initiators()
|
||||
|
||||
def validate_initiators(self):
|
||||
"""Validates the list of requested initiator HBAs to be used
|
||||
when estabilishing iSCSI sessions.
|
||||
"""
|
||||
valid_initiator_list = True
|
||||
if not self.initiator_list:
|
||||
LOG.info(_LI("No iSCSI initiator was explicitly requested. "
|
||||
"The Microsoft iSCSI initiator will choose the "
|
||||
"initiator when estabilishing sessions."))
|
||||
else:
|
||||
available_initiators = self._iscsi_utils.get_iscsi_initiators()
|
||||
for initiator in self.initiator_list:
|
||||
if initiator not in available_initiators:
|
||||
msg = _LW("The requested initiator %(req_initiator)s "
|
||||
"is not in the list of available initiators: "
|
||||
"%(avail_initiators)s.")
|
||||
LOG.warning(msg,
|
||||
dict(req_initiator=initiator,
|
||||
avail_initiators=available_initiators))
|
||||
valid_initiator_list = False
|
||||
return valid_initiator_list
|
||||
|
||||
def get_initiator(self):
|
||||
"""Returns the iSCSI initiator node name."""
|
||||
return self._iscsi_utils.get_iscsi_initiator()
|
||||
|
||||
@staticmethod
|
||||
def get_connector_properties(*args, **kwargs):
|
||||
iscsi_utils = utilsfactory.get_iscsi_initiator_utils()
|
||||
initiator = iscsi_utils.get_iscsi_initiator()
|
||||
return dict(initiator=initiator)
|
||||
|
||||
def _get_all_paths(self, connection_properties):
|
||||
initiator_list = self.initiator_list or [None]
|
||||
all_targets = self._get_all_targets(connection_properties)
|
||||
paths = [(initiator_name, target_portal, target_iqn, target_lun)
|
||||
for target_portal, target_iqn, target_lun in all_targets
|
||||
for initiator_name in initiator_list]
|
||||
return paths
|
||||
|
||||
def connect_volume(self, connection_properties):
|
||||
volume_connected = False
|
||||
for (initiator_name,
|
||||
target_portal,
|
||||
target_iqn,
|
||||
target_lun) in self._get_all_paths(connection_properties):
|
||||
try:
|
||||
msg = _LI("Attempting to establish an iSCSI session to "
|
||||
"target %(target_iqn)s on portal %(target_portal)s "
|
||||
"acessing LUN %(target_lun)s using initiator "
|
||||
"%(initiator_name)s.")
|
||||
LOG.info(msg, dict(target_portal=target_portal,
|
||||
target_iqn=target_iqn,
|
||||
target_lun=target_lun,
|
||||
initiator_name=initiator_name))
|
||||
self._iscsi_utils.login_storage_target(
|
||||
target_lun=target_lun,
|
||||
target_iqn=target_iqn,
|
||||
target_portal=target_portal,
|
||||
auth_username=connection_properties.get('auth_username'),
|
||||
auth_password=connection_properties.get('auth_password'),
|
||||
mpio_enabled=self.use_multipath,
|
||||
initiator_name=initiator_name,
|
||||
rescan_attempts=self.device_scan_attempts)
|
||||
|
||||
if not volume_connected:
|
||||
(device_number,
|
||||
device_path) = (
|
||||
self._iscsi_utils.get_device_number_and_path(
|
||||
target_iqn, target_lun))
|
||||
volume_connected = True
|
||||
|
||||
if not self.use_multipath:
|
||||
break
|
||||
except os_win_exc.OSWinException:
|
||||
LOG.exception(_LE("Could not estabilish the iSCSI session."))
|
||||
|
||||
if not volume_connected:
|
||||
raise exception.BrickException(
|
||||
_("Could not connect volume %s.") % connection_properties)
|
||||
|
||||
scsi_wwn = self._get_scsi_wwn(device_number)
|
||||
|
||||
device_info = {'type': 'block',
|
||||
'path': device_path,
|
||||
'number': device_number,
|
||||
'scsi_wwn': scsi_wwn}
|
||||
return device_info
|
||||
|
||||
def disconnect_volume(self, connection_properties):
|
||||
# We want to refresh the cached information first.
|
||||
self._diskutils.rescan_disks()
|
||||
for (target_portal,
|
||||
target_iqn,
|
||||
target_lun) in self._get_all_targets(connection_properties):
|
||||
|
||||
luns = self._iscsi_utils.get_target_luns(target_iqn)
|
||||
# We disconnect the target only if it does not expose other
|
||||
# luns which may be in use.
|
||||
if not luns or luns == [target_lun]:
|
||||
self._iscsi_utils.logout_storage_target(target_iqn)
|
||||
|
||||
def get_volume_paths(self, connection_properties):
|
||||
device_paths = set()
|
||||
|
||||
for (target_portal,
|
||||
target_iqn,
|
||||
target_lun) in self._get_all_targets(connection_properties):
|
||||
|
||||
(device_number,
|
||||
device_path) = self._iscsi_utils.get_device_number_and_path(
|
||||
target_iqn, target_lun)
|
||||
if device_path:
|
||||
device_paths.add(device_path)
|
||||
|
||||
self._check_device_paths(device_paths)
|
||||
return list(device_paths)
|
@ -260,6 +260,65 @@ class ConnectorTestCase(base.TestCase):
|
||||
self.assertFalse(self.connector.check_valid_device('/dev'))
|
||||
|
||||
|
||||
class BaseISCSIConnectorTestCase(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseISCSIConnectorTestCase, self).setUp()
|
||||
self.connector = connector.FakeBaseISCSIConnector(None)
|
||||
|
||||
@mock.patch.object(connector.BaseISCSIConnector, '_get_all_targets')
|
||||
def test_iterate_all_targets(self, mock_get_all_targets):
|
||||
# extra_property cannot be a sentinel, a copied sentinel will not
|
||||
# identical to the original one.
|
||||
connection_properties = {
|
||||
'target_portals': mock.sentinel.target_portals,
|
||||
'target_iqns': mock.sentinel.target_iqns,
|
||||
'target_luns': mock.sentinel.target_luns,
|
||||
'extra_property': 'extra_property'}
|
||||
mock_get_all_targets.return_value = [(
|
||||
mock.sentinel.portal, mock.sentinel.iqn, mock.sentinel.lun)]
|
||||
|
||||
# method is a generator, and it yields dictionaries. list() will
|
||||
# iterate over all of the method's items.
|
||||
list_props = list(
|
||||
self.connector._iterate_all_targets(connection_properties))
|
||||
|
||||
mock_get_all_targets.assert_called_once_with(connection_properties)
|
||||
self.assertEqual(1, len(list_props))
|
||||
|
||||
expected_props = {'target_portal': mock.sentinel.portal,
|
||||
'target_iqn': mock.sentinel.iqn,
|
||||
'target_lun': mock.sentinel.lun,
|
||||
'extra_property': 'extra_property'}
|
||||
self.assertDictEqual(expected_props, list_props[0])
|
||||
|
||||
def test_get_all_targets(self):
|
||||
connection_properties = {
|
||||
'target_portals': [mock.sentinel.target_portals],
|
||||
'target_iqns': [mock.sentinel.target_iqns],
|
||||
'target_luns': [mock.sentinel.target_luns]}
|
||||
|
||||
all_targets = self.connector._get_all_targets(connection_properties)
|
||||
|
||||
expected_targets = zip([mock.sentinel.target_portals],
|
||||
[mock.sentinel.target_iqns],
|
||||
[mock.sentinel.target_luns])
|
||||
self.assertEqual(list(expected_targets), list(all_targets))
|
||||
|
||||
def test_get_all_targets_single_target(self):
|
||||
connection_properties = {
|
||||
'target_portal': mock.sentinel.target_portal,
|
||||
'target_iqn': mock.sentinel.target_iqn,
|
||||
'target_lun': mock.sentinel.target_lun}
|
||||
|
||||
all_targets = self.connector._get_all_targets(connection_properties)
|
||||
|
||||
expected_target = (mock.sentinel.target_portal,
|
||||
mock.sentinel.target_iqn,
|
||||
mock.sentinel.target_lun)
|
||||
self.assertEqual([expected_target], all_targets)
|
||||
|
||||
|
||||
class ISCSIConnectorTestCase(ConnectorTestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
0
os_brick/tests/windows/__init__.py
Normal file
0
os_brick/tests/windows/__init__.py
Normal file
33
os_brick/tests/windows/fake_win_conn.py
Normal file
33
os_brick/tests/windows/fake_win_conn.py
Normal file
@ -0,0 +1,33 @@
|
||||
# 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 os_brick.initiator.windows import base as win_conn_base
|
||||
|
||||
|
||||
class FakeWindowsConnector(win_conn_base.BaseWindowsConnector):
|
||||
def connect_volume(self, connection_properties):
|
||||
return {}
|
||||
|
||||
def disconnect_volume(self, connection_properties, device_info):
|
||||
pass
|
||||
|
||||
def get_volume_paths(self, connection_properties):
|
||||
return []
|
||||
|
||||
def get_search_path(self):
|
||||
return None
|
||||
|
||||
def get_all_available_volumes(self, connection_properties=None):
|
||||
return []
|
34
os_brick/tests/windows/test_base.py
Normal file
34
os_brick/tests/windows/test_base.py
Normal file
@ -0,0 +1,34 @@
|
||||
# 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 mock
|
||||
from os_win import utilsfactory
|
||||
|
||||
from os_brick.tests import base
|
||||
|
||||
|
||||
class WindowsConnectorTestBase(base.TestCase):
|
||||
@mock.patch('sys.platform', 'win32')
|
||||
def setUp(self):
|
||||
super(WindowsConnectorTestBase, self).setUp()
|
||||
|
||||
# All the Windows connectors use os_win.utilsfactory to fetch Windows
|
||||
# specific utils. During init, those will run methods that will fail
|
||||
# on other platforms. To make testing easier and avoid checking the
|
||||
# platform in the code, we can simply mock this factory method.
|
||||
utilsfactory_patcher = mock.patch.object(
|
||||
utilsfactory, '_get_class')
|
||||
utilsfactory_patcher.start()
|
||||
self.addCleanup(utilsfactory_patcher.stop)
|
134
os_brick/tests/windows/test_base_connector.py
Normal file
134
os_brick/tests/windows/test_base_connector.py
Normal file
@ -0,0 +1,134 @@
|
||||
# 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
|
||||
import six
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.initiator.windows import base as base_win_conn
|
||||
from os_brick.tests.windows import fake_win_conn
|
||||
from os_brick.tests.windows import test_base
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class BaseWindowsConnectorTestCase(test_base.WindowsConnectorTestBase):
|
||||
def setUp(self):
|
||||
super(BaseWindowsConnectorTestCase, self).setUp()
|
||||
|
||||
self._diskutils = mock.Mock()
|
||||
|
||||
self._connector = fake_win_conn.FakeWindowsConnector()
|
||||
self._connector._diskutils = self._diskutils
|
||||
|
||||
@ddt.data({},
|
||||
{'feature_available': True},
|
||||
{'feature_available': False, 'enforce_multipath': True})
|
||||
@ddt.unpack
|
||||
@mock.patch.object(base_win_conn.utilsfactory, 'get_hostutils')
|
||||
def test_check_multipath_support(self, mock_get_hostutils,
|
||||
feature_available=True,
|
||||
enforce_multipath=False):
|
||||
mock_hostutils = mock_get_hostutils.return_value
|
||||
mock_hostutils.check_server_feature.return_value = feature_available
|
||||
check_mpio = base_win_conn.BaseWindowsConnector.check_multipath_support
|
||||
|
||||
if feature_available or not enforce_multipath:
|
||||
multipath_support = check_mpio(
|
||||
enforce_multipath=enforce_multipath)
|
||||
self.assertEqual(feature_available, multipath_support)
|
||||
else:
|
||||
self.assertRaises(exception.BrickException,
|
||||
check_mpio,
|
||||
enforce_multipath=enforce_multipath)
|
||||
mock_hostutils.check_server_feature.assert_called_once_with(
|
||||
mock_hostutils.FEATURE_MPIO)
|
||||
|
||||
@ddt.data({}, {'mpio_requested': False}, {'mpio_available': True})
|
||||
@mock.patch.object(base_win_conn.BaseWindowsConnector,
|
||||
'check_multipath_support')
|
||||
@ddt.unpack
|
||||
def test_get_connector_properties(self, mock_check_mpio,
|
||||
mpio_requested=True,
|
||||
mpio_available=True):
|
||||
mock_check_mpio.return_value = mpio_available
|
||||
enforce_multipath = False
|
||||
|
||||
props = base_win_conn.BaseWindowsConnector.get_connector_properties(
|
||||
multipath=mpio_requested,
|
||||
enforce_multipath=enforce_multipath)
|
||||
self.assertEqual(mpio_requested and mpio_available,
|
||||
props['multipath'])
|
||||
if mpio_requested:
|
||||
mock_check_mpio.assert_called_once_with(enforce_multipath)
|
||||
|
||||
def test_get_scsi_wwn(self):
|
||||
mock_get_uid_and_type = self._diskutils.get_disk_uid_and_uid_type
|
||||
mock_get_uid_and_type.return_value = (mock.sentinel.disk_uid,
|
||||
mock.sentinel.uid_type)
|
||||
|
||||
scsi_wwn = self._connector._get_scsi_wwn(mock.sentinel.dev_num)
|
||||
expected_wwn = '%s%s' % (mock.sentinel.uid_type,
|
||||
mock.sentinel.disk_uid)
|
||||
self.assertEqual(expected_wwn, scsi_wwn)
|
||||
mock_get_uid_and_type.assert_called_once_with(mock.sentinel.dev_num)
|
||||
|
||||
@ddt.data(None, IOError)
|
||||
@mock.patch.object(six.moves.builtins, 'open')
|
||||
def test_check_valid_device(self, exc, mock_open):
|
||||
mock_open.side_effect = exc
|
||||
|
||||
valid_device = self._connector.check_valid_device(
|
||||
mock.sentinel.dev_path)
|
||||
self.assertEqual(not exc, valid_device)
|
||||
|
||||
mock_open.assert_any_call(mock.sentinel.dev_path, 'r')
|
||||
mock_read = mock_open.return_value.__enter__.return_value.read
|
||||
if not exc:
|
||||
mock_read.assert_called_once_with(1)
|
||||
|
||||
def test_check_device_paths(self):
|
||||
# We expect an exception to be raised if the same volume
|
||||
# can be accessed through multiple paths.
|
||||
device_paths = [mock.sentinel.dev_path_0,
|
||||
mock.sentinel.dev_path_1]
|
||||
self.assertRaises(exception.BrickException,
|
||||
self._connector._check_device_paths,
|
||||
device_paths)
|
||||
|
||||
@mock.patch.object(fake_win_conn.FakeWindowsConnector,
|
||||
'get_volume_paths')
|
||||
def test_extend_volume(self, mock_get_vol_paths):
|
||||
mock_vol_paths = [mock.sentinel.dev_path]
|
||||
mock_get_vol_paths.return_value = mock_vol_paths
|
||||
|
||||
self._connector.extend_volume(mock.sentinel.conn_props)
|
||||
|
||||
mock_get_vol_paths.assert_called_once_with(mock.sentinel.conn_props)
|
||||
mock_get_dev_num = self._diskutils.get_device_number_from_device_name
|
||||
mock_get_dev_num.assert_called_once_with(mock.sentinel.dev_path)
|
||||
self._diskutils.refresh_disk.assert_called_once_with(
|
||||
mock_get_dev_num.return_value)
|
||||
|
||||
@mock.patch.object(fake_win_conn.FakeWindowsConnector,
|
||||
'get_volume_paths')
|
||||
def test_extend_volume_missing_path(self, mock_get_vol_paths):
|
||||
mock_get_vol_paths.return_value = []
|
||||
|
||||
self.assertRaises(exception.NotFound,
|
||||
self._connector.extend_volume,
|
||||
mock.sentinel.conn_props)
|
||||
|
||||
mock_get_vol_paths.assert_called_once_with(mock.sentinel.conn_props)
|
32
os_brick/tests/windows/test_factory.py
Normal file
32
os_brick/tests/windows/test_factory.py
Normal file
@ -0,0 +1,32 @@
|
||||
# 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.initiator import connector
|
||||
from os_brick.initiator.windows import iscsi
|
||||
from os_brick.tests.windows import test_base
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class WindowsConnectorFactoryTestCase(test_base.WindowsConnectorTestBase):
|
||||
@ddt.data({'proto': connector.ISCSI,
|
||||
'expected_cls': iscsi.WindowsISCSIConnector})
|
||||
@ddt.unpack
|
||||
@mock.patch('sys.platform', 'win32')
|
||||
def test_factory(self, proto, expected_cls):
|
||||
obj = connector.InitiatorConnector.factory(proto, None)
|
||||
self.assertIsInstance(obj, expected_cls)
|
190
os_brick/tests/windows/test_iscsi.py
Normal file
190
os_brick/tests/windows/test_iscsi.py
Normal file
@ -0,0 +1,190 @@
|
||||
# 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_win import exceptions as os_win_exc
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.initiator.windows import iscsi
|
||||
from os_brick.tests.windows import test_base
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class WindowsISCSIConnectorTestCase(test_base.WindowsConnectorTestBase):
|
||||
@mock.patch.object(iscsi.WindowsISCSIConnector, 'validate_initiators')
|
||||
def setUp(self, mock_validate_connectors):
|
||||
super(WindowsISCSIConnectorTestCase, self).setUp()
|
||||
|
||||
self._diskutils = mock.Mock()
|
||||
self._iscsi_utils = mock.Mock()
|
||||
|
||||
self._connector = iscsi.WindowsISCSIConnector()
|
||||
self._connector._diskutils = self._diskutils
|
||||
self._connector._iscsi_utils = self._iscsi_utils
|
||||
|
||||
@ddt.data({'requested_initiators': [mock.sentinel.initiator_0],
|
||||
'available_initiators': [mock.sentinel.initiator_0,
|
||||
mock.sentinel.initiator_1]},
|
||||
{'requested_initiators': [mock.sentinel.initiator_0],
|
||||
'available_initiators': [mock.sentinel.initiator_1]},
|
||||
{'requested_initiators': [],
|
||||
'available_initiators': [mock.sentinel.software_initiator]})
|
||||
@ddt.unpack
|
||||
def test_validate_initiators(self, requested_initiators,
|
||||
available_initiators):
|
||||
self._iscsi_utils.get_iscsi_initiators.return_value = (
|
||||
available_initiators)
|
||||
self._connector.initiator_list = requested_initiators
|
||||
|
||||
expected_valid_initiator = not (
|
||||
set(requested_initiators).difference(set(available_initiators)))
|
||||
valid_initiator = self._connector.validate_initiators()
|
||||
|
||||
self.assertEqual(expected_valid_initiator, valid_initiator)
|
||||
|
||||
def test_get_initiator(self):
|
||||
initiator = self._connector.get_initiator()
|
||||
self.assertEqual(self._iscsi_utils.get_iscsi_initiator.return_value,
|
||||
initiator)
|
||||
|
||||
@mock.patch.object(iscsi, 'utilsfactory')
|
||||
def test_get_connector_properties(self, mock_utilsfactory):
|
||||
mock_iscsi_utils = (
|
||||
mock_utilsfactory.get_iscsi_initiator_utils.return_value)
|
||||
|
||||
props = self._connector.get_connector_properties()
|
||||
expected_props = dict(
|
||||
initiator=mock_iscsi_utils.get_iscsi_initiator.return_value)
|
||||
|
||||
self.assertEqual(expected_props, props)
|
||||
|
||||
@mock.patch.object(iscsi.WindowsISCSIConnector, '_get_all_targets')
|
||||
def test_get_all_paths(self, mock_get_all_targets):
|
||||
initiators = [mock.sentinel.initiator_0, mock.sentinel.initiator_1]
|
||||
all_targets = [(mock.sentinel.portal_0, mock.sentinel.target_0,
|
||||
mock.sentinel.lun_0),
|
||||
(mock.sentinel.portal_1, mock.sentinel.target_1,
|
||||
mock.sentinel.lun_1)]
|
||||
|
||||
self._connector.initiator_list = initiators
|
||||
mock_get_all_targets.return_value = all_targets
|
||||
|
||||
expected_paths = [
|
||||
(initiator_name, target_portal, target_iqn, target_lun)
|
||||
for target_portal, target_iqn, target_lun in all_targets
|
||||
for initiator_name in initiators]
|
||||
all_paths = self._connector._get_all_paths(mock.sentinel.conn_props)
|
||||
|
||||
self.assertEqual(expected_paths, all_paths)
|
||||
mock_get_all_targets.assert_called_once_with(mock.sentinel.conn_props)
|
||||
|
||||
@ddt.data(True, False)
|
||||
@mock.patch.object(iscsi.WindowsISCSIConnector, '_get_scsi_wwn')
|
||||
@mock.patch.object(iscsi.WindowsISCSIConnector, '_get_all_paths')
|
||||
def test_connect_volume(self, use_multipath,
|
||||
mock_get_all_paths, mock_get_scsi_wwn):
|
||||
fake_paths = [(mock.sentinel.initiator_name,
|
||||
mock.sentinel.target_portal,
|
||||
mock.sentinel.target_iqn,
|
||||
mock.sentinel.target_lun)] * 3
|
||||
fake_conn_props = dict(auth_username=mock.sentinel.auth_username,
|
||||
auth_password=mock.sentinel.auth_password)
|
||||
|
||||
mock_get_all_paths.return_value = fake_paths
|
||||
self._iscsi_utils.login_storage_target.side_effect = [
|
||||
os_win_exc.OSWinException, None, None]
|
||||
self._iscsi_utils.get_device_number_and_path.return_value = (
|
||||
mock.sentinel.device_number, mock.sentinel.device_path)
|
||||
self._connector.use_multipath = use_multipath
|
||||
|
||||
device_info = self._connector.connect_volume(fake_conn_props)
|
||||
expected_device_info = dict(type='block',
|
||||
path=mock.sentinel.device_path,
|
||||
number=mock.sentinel.device_number,
|
||||
scsi_wwn=mock_get_scsi_wwn.return_value)
|
||||
|
||||
self.assertEqual(expected_device_info, device_info)
|
||||
|
||||
mock_get_all_paths.assert_called_once_with(fake_conn_props)
|
||||
expected_login_attempts = 3 if use_multipath else 2
|
||||
self._iscsi_utils.login_storage_target.assert_has_calls(
|
||||
[mock.call(target_lun=mock.sentinel.target_lun,
|
||||
target_iqn=mock.sentinel.target_iqn,
|
||||
target_portal=mock.sentinel.target_portal,
|
||||
auth_username=mock.sentinel.auth_username,
|
||||
auth_password=mock.sentinel.auth_password,
|
||||
mpio_enabled=use_multipath,
|
||||
initiator_name=mock.sentinel.initiator_name,
|
||||
rescan_attempts=(
|
||||
self._connector.device_scan_attempts))] *
|
||||
expected_login_attempts)
|
||||
|
||||
self._iscsi_utils.get_device_number_and_path.assert_called_once_with(
|
||||
mock.sentinel.target_iqn, mock.sentinel.target_lun)
|
||||
mock_get_scsi_wwn.assert_called_once_with(mock.sentinel.device_number)
|
||||
|
||||
@mock.patch.object(iscsi.WindowsISCSIConnector, '_get_all_paths')
|
||||
def test_connect_volume_exc(self, mock_get_all_paths):
|
||||
fake_paths = [(mock.sentinel.initiator_name,
|
||||
mock.sentinel.target_portal,
|
||||
mock.sentinel.target_iqn,
|
||||
mock.sentinel.target_lun)] * 3
|
||||
|
||||
mock_get_all_paths.return_value = fake_paths
|
||||
self._iscsi_utils.login_storage_target.side_effect = (
|
||||
os_win_exc.OSWinException)
|
||||
self._connector.use_multipath = True
|
||||
|
||||
self.assertRaises(exception.BrickException,
|
||||
self._connector.connect_volume,
|
||||
connection_properties={})
|
||||
|
||||
@mock.patch.object(iscsi.WindowsISCSIConnector, '_get_all_targets')
|
||||
def test_disconnect_volume(self, mock_get_all_targets):
|
||||
targets = [
|
||||
(mock.sentinel.portal_0, mock.sentinel.tg_0, mock.sentinel.lun_0),
|
||||
(mock.sentinel.portal_1, mock.sentinel.tg_1, mock.sentinel.lun_1)]
|
||||
|
||||
mock_get_all_targets.return_value = targets
|
||||
self._iscsi_utils.get_target_luns.return_value = [mock.sentinel.lun_0]
|
||||
|
||||
self._connector.disconnect_volume(mock.sentinel.conn_props)
|
||||
|
||||
self._diskutils.rescan_disks.assert_called_once_with()
|
||||
mock_get_all_targets.assert_called_once_with(mock.sentinel.conn_props)
|
||||
self._iscsi_utils.logout_storage_target.assert_called_once_with(
|
||||
mock.sentinel.tg_0)
|
||||
self._iscsi_utils.get_target_luns.assert_has_calls(
|
||||
[mock.call(mock.sentinel.tg_0), mock.call(mock.sentinel.tg_1)])
|
||||
|
||||
@mock.patch.object(iscsi.WindowsISCSIConnector, '_get_all_targets')
|
||||
@mock.patch.object(iscsi.WindowsISCSIConnector, '_check_device_paths')
|
||||
def test_get_volume_paths(self, mock_check_dev_paths,
|
||||
mock_get_all_targets):
|
||||
targets = [
|
||||
(mock.sentinel.portal_0, mock.sentinel.tg_0, mock.sentinel.lun_0),
|
||||
(mock.sentinel.portal_1, mock.sentinel.tg_1, mock.sentinel.lun_1)]
|
||||
|
||||
mock_get_all_targets.return_value = targets
|
||||
self._iscsi_utils.get_device_number_and_path.return_value = [
|
||||
mock.sentinel.dev_num, mock.sentinel.dev_path]
|
||||
|
||||
volume_paths = self._connector.get_volume_paths(
|
||||
mock.sentinel.conn_props)
|
||||
expected_paths = [mock.sentinel.dev_path]
|
||||
|
||||
self.assertEqual(expected_paths, volume_paths)
|
||||
mock_check_dev_paths.assert_called_once_with(set(expected_paths))
|
@ -16,3 +16,4 @@ requests>=2.10.0 # Apache-2.0
|
||||
retrying!=1.3.0,>=1.2.3 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
||||
castellan>=0.4.0 # Apache-2.0
|
||||
os-win>=0.2.3 # Apache-2.0
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
hacking<0.11,>=0.10.0
|
||||
coverage>=3.6 # Apache-2.0
|
||||
ddt>=1.0.1 # MIT
|
||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||
reno>=1.6.2 # Apache2
|
||||
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
|
||||
|
Loading…
Reference in New Issue
Block a user