Add Windows iSCSI connector

This patch adds a Windows iSCSI connector while the following
changes will add SMBFS and Fibre Channel connectors as well.

os-win is added as a requirement, as well as ddt. Note that
both are in the global requirements list. os-win is under OpenStack
governance and already being used by multiple OpenStack projects
such as Nova and Cinder.

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

Change-Id: I19dfc8dd2e9e8a1b17675b55c63de903804480e4
Partial-Implements: blueprint os-brick-windows-support
This commit is contained in:
Lucian Petrut 2016-01-26 14:50:18 +02:00 committed by Claudiu Belu
parent f359ecc3f7
commit 585445eecf
13 changed files with 830 additions and 23 deletions

View File

@ -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')]):

View 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)

View 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

View 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)

View File

@ -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):

View File

View 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 []

View 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)

View 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)

View 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)

View 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))

View File

@ -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

View File

@ -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