Add iSCSI MPIO support

Until now, even if there were multiple paths on which we could
access a volume, we were estabilishing a single iSCSI session.

This patch changes this, so we may use all the targets/portals
that we retrieve in the connection info.

Also, a config option was added so that the deployer can request
which initiators will be used for estabilishing the session, for
example forcing some hardware initiators to be used. If none is
explicitly requested, we let the iSCSI initiator service pick one.

This will also require the MPIO service to be enabled and properly
configured, in order to claim the disks.

Change-Id: Ia5c0aa81d56bb862b3b30de4b782d9d1d15d44f5
This commit is contained in:
Lucian Petrut 2016-03-03 14:15:10 +02:00
parent 4bbba46014
commit af8f9928bd
3 changed files with 374 additions and 346 deletions

View File

@ -23,12 +23,12 @@ import os
import platform
import re
import sys
import time
from nova import block_device
from nova import exception
from nova import utils
from nova.virt import driver
from os_win import exceptions as os_win_exc
from os_win import utilsfactory
from oslo_config import cfg
from oslo_log import log as logging
@ -36,9 +36,8 @@ from oslo_service import loopingcall
from oslo_utils import excutils
from oslo_utils import units
import six
from six.moves import range
from hyperv.i18n import _, _LE, _LW
from hyperv.i18n import _, _LI, _LE, _LW
from hyperv.nova import constants
LOG = logging.getLogger(__name__)
@ -64,6 +63,12 @@ hyper_volumeops_opts = [
'FC disks. This requires the Multipath IO Windows '
'feature to be enabled. MPIO must be configured to '
'claim such devices.'),
cfg.ListOpt('iscsi_initiator_list',
default=[],
help='List of iSCSI initiators that will be used for '
'estabilishing iSCSI sessions. If none is specified, '
'the Microsoft iSCSI initiator service will choose '
'the initiator.'),
]
CONF = cfg.CONF
@ -102,9 +107,8 @@ class VolumeOps(object):
"enabled. MPIO must be configured to claim such devices.")
raise exception.ServiceUnavailable(err_msg)
def _get_volume_driver(self, driver_type=None, connection_info=None):
if connection_info:
driver_type = connection_info.get('driver_volume_type')
def _get_volume_driver(self, connection_info):
driver_type = connection_info.get('driver_volume_type')
if driver_type not in self.volume_drivers:
raise exception.VolumeDriverNotFound(driver_type=driver_type)
return self.volume_drivers[driver_type]
@ -115,8 +119,7 @@ class VolumeOps(object):
def attach_volume(self, connection_info, instance_name,
disk_bus=constants.CTRL_TYPE_SCSI):
volume_driver = self._get_volume_driver(
connection_info=connection_info)
volume_driver = self._get_volume_driver(connection_info)
volume_connected = False
try:
@ -144,15 +147,13 @@ class VolumeOps(object):
def disconnect_volumes(self, block_device_info):
mapping = driver.block_device_info_get_mapping(block_device_info)
block_devices = self._group_block_devices_by_type(
mapping)
for driver_type, block_device_mapping in six.iteritems(block_devices):
volume_driver = self._get_volume_driver(driver_type)
volume_driver.disconnect_volumes(block_device_mapping)
for volume in mapping:
connection_info = volume['connection_info']
volume_driver = self._get_volume_driver(connection_info)
volume_driver.disconnect_volume(connection_info)
def detach_volume(self, connection_info, instance_name):
volume_driver = self._get_volume_driver(
connection_info=connection_info)
volume_driver = self._get_volume_driver(connection_info)
volume_driver.detach_volume(connection_info, instance_name)
volume_driver.disconnect_volume(connection_info)
@ -201,8 +202,7 @@ class VolumeOps(object):
mapping = driver.block_device_info_get_mapping(block_device_info)
for vol in mapping:
connection_info = vol['connection_info']
volume_driver = self._get_volume_driver(
connection_info=connection_info)
volume_driver = self._get_volume_driver(connection_info)
volume_driver.connect_volume(connection_info)
def parse_disk_qos_specs(self, qos_specs):
@ -252,17 +252,8 @@ class VolumeOps(object):
disk_path_mapping[disk_serial] = disk_path
return disk_path_mapping
def _group_block_devices_by_type(self, block_device_mapping):
block_devices = collections.defaultdict(list)
for volume in block_device_mapping:
connection_info = volume['connection_info']
volume_type = connection_info.get('driver_volume_type')
block_devices[volume_type].append(volume)
return block_devices
def get_disk_resource_path(self, connection_info):
volume_driver = self._get_volume_driver(
connection_info=connection_info)
volume_driver = self._get_volume_driver(connection_info)
return volume_driver.get_disk_resource_path(connection_info)
@ -270,14 +261,12 @@ class VolumeOps(object):
class BaseVolumeDriver(object):
def __init__(self):
self._vmutils = utilsfactory.get_vmutils()
self._diskutils = utilsfactory.get_diskutils()
self._is_block_dev = True
def connect_volume(self, connection_info):
pass
def disconnect_volumes(self, block_device_info):
pass
def disconnect_volume(self, connection_info):
pass
@ -346,133 +335,160 @@ class BaseVolumeDriver(object):
LOG.warn(_LW("The %s Hyper-V volume driver does not support QoS. "
"Ignoring QoS specs."), volume_type)
def _check_device_paths(self, device_paths):
if len(device_paths) > 1:
err_msg = _("Multiple disk 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.InvalidDevicePath(err_msg % device_paths)
elif not device_paths:
err_msg = _("Could not find the physical disk "
"path for the requested volume.")
raise exception.DiskNotFound(err_msg)
def _get_mounted_disk_path_by_dev_name(self, device_name):
device_number = self._diskutils.get_device_number_from_device_name(
device_name)
mounted_disk_path = self._vmutils.get_mounted_disk_by_drive_number(
device_number)
return mounted_disk_path
class ISCSIVolumeDriver(BaseVolumeDriver):
def __init__(self):
self._volutils = utilsfactory.get_iscsi_initiator_utils()
self._initiator = self._volutils.get_iscsi_initiator()
super(ISCSIVolumeDriver, self).__init__()
self._iscsi_utils = utilsfactory.get_iscsi_initiator_utils()
self._initiator_node_name = self._iscsi_utils.get_iscsi_initiator()
self.validate_initiators()
def get_volume_connector_props(self):
props = {'initiator': self._initiator}
props = {'initiator': self._initiator_node_name}
return props
def login_storage_target(self, connection_info):
data = connection_info['data']
target_lun = data['target_lun']
target_iqn = data['target_iqn']
target_portal = data['target_portal']
auth_method = data.get('auth_method')
auth_username = data.get('auth_username')
auth_password = data.get('auth_password')
def validate_initiators(self):
# The MS iSCSI initiator service can manage the software iSCSI
# initiator as well as hardware initiators.
initiator_list = CONF.hyperv.iscsi_initiator_list
valid_initiators = True
if not 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 initiator_list:
if initiator not in available_initiators:
valid_initiators = False
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))
return valid_initiators
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 _get_all_paths(self, connection_properties):
initiator_list = CONF.hyperv.iscsi_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_info):
connection_properties = connection_info['data']
auth_method = connection_properties.get('auth_method')
if auth_method and auth_method.upper() != 'CHAP':
LOG.error(_LE("Cannot log in target %(target_iqn)s. Unsupported "
"iSCSI authentication method: %(auth_method)s."),
{'target_iqn': target_iqn,
'auth_method': auth_method})
LOG.error(_LE("Unsupported iSCSI authentication "
"method: %(auth_method)s."),
dict(auth_method=auth_method))
raise exception.UnsupportedBDMVolumeAuthMethod(
auth_method=auth_method)
# Check if we already logged in
if self._volutils.get_device_number_for_target(target_iqn, target_lun):
LOG.debug("Already logged in on storage target. No need to "
"login. Portal: %(target_portal)s, "
"IQN: %(target_iqn)s, LUN: %(target_lun)s",
{'target_portal': target_portal,
'target_iqn': target_iqn, 'target_lun': target_lun})
else:
LOG.debug("Logging in on storage target. Portal: "
"%(target_portal)s, IQN: %(target_iqn)s, "
"LUN: %(target_lun)s",
{'target_portal': target_portal,
'target_iqn': target_iqn, 'target_lun': target_lun})
self._volutils.login_storage_target(target_lun, target_iqn,
target_portal, auth_username,
auth_password)
# Wait for the target to be mounted
self._get_mounted_disk_from_lun(target_iqn, target_lun, True)
volume_connected = False
for (initiator_name,
target_portal,
target_iqn,
target_lun) in self._get_all_paths(connection_properties):
try:
msg = _LI("Attempting to estabilish 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=CONF.hyperv.use_multipath_io,
initiator_name=initiator_name)
def disconnect_volumes(self, block_device_mapping):
iscsi_targets = collections.defaultdict(int)
for vol in block_device_mapping:
target_iqn = vol['connection_info']['data']['target_iqn']
iscsi_targets[target_iqn] += 1
volume_connected = True
if not CONF.hyperv.use_multipath_io:
break
except os_win_exc.OSWinException:
LOG.exception(_LE("Could not connect iSCSI target %s."),
target_iqn)
for target_iqn, disconnected_luns in six.iteritems(iscsi_targets):
self.logout_storage_target(target_iqn, disconnected_luns)
if not volume_connected:
raise exception.VolumeAttachFailed(
_("Could not connect volume %s.") %
connection_properties['volume_id'])
def disconnect_volume(self, connection_info):
target_iqn = connection_info['data']['target_iqn']
self.logout_storage_target(target_iqn)
# We want to refresh the cached information first.
self._diskutils.rescan_disks()
def logout_storage_target(self, target_iqn, disconnected_luns_count=1):
total_available_luns = self._volutils.get_target_lun_count(
target_iqn)
for (target_portal,
target_iqn,
target_lun) in self._get_all_targets(connection_info['data']):
if total_available_luns == disconnected_luns_count:
LOG.debug("Logging off storage target %s", target_iqn)
self._volutils.logout_storage_target(target_iqn)
else:
LOG.debug("Skipping disconnecting target %s as there "
"are LUNs still being used.", target_iqn)
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_disk_resource_path(self, connection_info):
data = connection_info['data']
target_lun = data['target_lun']
target_iqn = data['target_iqn']
device_paths = set()
connection_properties = connection_info['data']
# Getting the mounted disk
return self._get_mounted_disk_from_lun(target_iqn, target_lun,
wait_for_device=True)
for (target_portal,
target_iqn,
target_lun) in self._get_all_targets(connection_properties):
def _get_mounted_disk_from_lun(self, target_iqn, target_lun,
wait_for_device=False):
# The WMI query in get_device_number_for_target can incorrectly
# return no data when the system is under load. This issue can
# be avoided by adding a retry.
for i in range(CONF.hyperv.mounted_disk_query_retry_count):
device_number = self._volutils.get_device_number_for_target(
(device_number,
device_path) = self._iscsi_utils.get_device_number_and_path(
target_iqn, target_lun)
if device_number in (None, -1):
attempt = i + 1
LOG.debug('Attempt %d to get device_number '
'from get_device_number_for_target failed. '
'Retrying...', attempt)
time.sleep(CONF.hyperv.mounted_disk_query_retry_interval)
else:
break
if device_path:
device_paths.add(device_path)
if device_number in (None, -1):
raise exception.NotFound(_('Unable to find a mounted disk for '
'target_iqn: %s') % target_iqn)
LOG.debug('Device number: %(device_number)s, '
'target lun: %(target_lun)s',
{'device_number': device_number, 'target_lun': target_lun})
# Finding Mounted disk drive
for i in range(0, CONF.hyperv.volume_attach_retry_count):
mounted_disk_path = self._vmutils.get_mounted_disk_by_drive_number(
device_number)
if mounted_disk_path or not wait_for_device:
break
time.sleep(CONF.hyperv.volume_attach_retry_interval)
if not mounted_disk_path:
raise exception.NotFound(_('Unable to find a mounted disk for '
'target_iqn: %s. Please ensure that '
'the host\'s SAN policy is set to '
'"OfflineAll" or "OfflineShared"') %
target_iqn)
return mounted_disk_path
def get_target_from_disk_path(self, physical_drive_path):
return self._volutils.get_target_from_disk_path(physical_drive_path)
def get_target_lun_count(self, target_iqn):
return self._volutils.get_target_lun_count(target_iqn)
def connect_volume(self, connection_info):
self.login_storage_target(connection_info)
self._check_device_paths(device_paths)
disk_path = list(device_paths)[0]
return self._get_mounted_disk_path_by_dev_name(disk_path)
def export_path_synchronized(f):
@ -497,16 +513,6 @@ class SMBFSVolumeDriver(BaseVolumeDriver):
def get_disk_resource_path(self, connection_info):
return self._get_disk_path(connection_info)
def disconnect_volumes(self, block_device_mapping):
export_paths = set()
for vol in block_device_mapping:
connection_info = vol['connection_info']
export_path = self._get_export_path(connection_info)
export_paths.add(export_path)
for export_path in export_paths:
self._unmount_smb_share(export_path)
def disconnect_volume(self, connection_info):
export_path = self._get_export_path(connection_info)
self._unmount_smb_share(export_path)
@ -584,6 +590,7 @@ class FCVolumeDriver(BaseVolumeDriver):
def get_disk_resource_path(self, connection_info):
@loopingcall.RetryDecorator(max_retry_count=10, max_sleep_time=0)
def get_disk_path():
disk_paths = set()
volume_mappings = self._get_fc_volume_mappings(connection_info)
if not volume_mappings:
LOG.debug("Could not find FC mappings for volume "
@ -598,8 +605,13 @@ class FCVolumeDriver(BaseVolumeDriver):
for mapping in volume_mappings:
device_name = mapping['device_name']
if device_name:
return self._get_mounted_disk_path_by_dev_name(
device_name)
disk_paths.add(device_name)
if disk_paths:
self._check_device_paths(disk_paths)
disk_path = list(disk_paths)[0]
return self._get_mounted_disk_path_by_dev_name(
disk_path)
err_msg = _("Could not find the physical disk "
"path for the requested volume.")
@ -607,13 +619,6 @@ class FCVolumeDriver(BaseVolumeDriver):
return get_disk_path()
def _get_mounted_disk_path_by_dev_name(self, device_name):
device_number = self._vmutils.get_device_number_from_device_name(
device_name)
mounted_disk_path = self._vmutils.get_mounted_disk_by_drive_number(
device_number)
return mounted_disk_path
def _get_fc_volume_mappings(self, connection_info):
# Note(lpetrut): All the WWNs returned by os-win are upper case.
target_wwpns = [wwpn.upper()

View File

@ -18,11 +18,13 @@ import os
import platform
import sys
import ddt
import mock
from oslo_config import cfg
from nova import exception
from nova.tests.unit import fake_block_device
from os_win import exceptions as os_win_exc
from oslo_utils import units
from hyperv.nova import constants
@ -32,12 +34,12 @@ from hyperv.tests.unit import test_base
CONF = cfg.CONF
connection_data = {'volume_id': 'fake_vol_id',
'target_lun': mock.sentinel.fake_lun,
'target_iqn': mock.sentinel.fake_iqn,
'target_portal': mock.sentinel.fake_portal,
'target_lun': mock.sentinel.target_lun,
'target_iqn': mock.sentinel.target_iqn,
'target_portal': mock.sentinel.target_portal,
'auth_method': 'chap',
'auth_username': mock.sentinel.fake_user,
'auth_password': mock.sentinel.fake_pass}
'auth_username': mock.sentinel.auth_username,
'auth_password': mock.sentinel.auth_password}
def get_fake_block_dev_info():
@ -167,8 +169,7 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
mock.sentinel.instance_name,
mock.sentinel.fake_disk_bus)
mock_get_volume_driver.assert_called_once_with(
connection_info=fake_conn_info)
mock_get_volume_driver.assert_called_once_with(fake_conn_info)
mock_volume_driver.attach_volume.assert_called_once_with(
fake_conn_info,
mock.sentinel.instance_name,
@ -190,7 +191,7 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
mock.sentinel.instance_name)
mock_get_volume_driver.assert_called_once_with(
connection_info=mock.sentinel.conn_info)
mock.sentinel.conn_info)
mock_volume_driver = mock_get_volume_driver.return_value
mock_volume_driver.detach_volume.assert_called_once_with(
mock.sentinel.conn_info, mock.sentinel.instance_name)
@ -200,13 +201,16 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
@mock.patch.object(volumeops.VolumeOps, '_get_volume_driver')
def test_disconnect_volumes(self, mock_get_volume_driver):
block_device_info = get_fake_block_dev_info()
block_device_mapping = block_device_info['block_device_mapping']
block_device_mapping[0]['connection_info'] = {
'driver_volume_type': mock.sentinel.fake_vol_type}
fake_volume_driver = mock_get_volume_driver.return_value
conn_info = block_device_info[
'block_device_mapping'][0]['connection_info']
self._volumeops.disconnect_volumes(block_device_info)
fake_volume_driver.disconnect_volumes.assert_called_once_with(
block_device_mapping)
mock_get_volume_driver.assert_called_once_with(conn_info)
disconnect_volume = (
mock_get_volume_driver.return_value.disconnect_volume)
disconnect_volume.assert_called_once_with(
conn_info)
@mock.patch('nova.block_device.volume_in_mapping')
def test_ebs_root_in_block_devices(self, mock_vol_in_mapping):
@ -244,13 +248,16 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
@mock.patch.object(volumeops.VolumeOps, '_get_volume_driver')
def test_connect_volumes(self, mock_get_volume_driver):
block_device_info = get_fake_block_dev_info()
conn_info = block_device_info[
'block_device_mapping'][0]['connection_info']
self._volumeops.connect_volumes(block_device_info)
init_vol_conn = (
mock_get_volume_driver.assert_called_once_with(conn_info)
connect_volume = (
mock_get_volume_driver.return_value.connect_volume)
init_vol_conn.assert_called_once_with(
block_device_info['block_device_mapping'][0]['connection_info'])
connect_volume.assert_called_once_with(
conn_info)
@mock.patch.object(volumeops.VolumeOps,
'get_disk_resource_path')
@ -272,16 +279,6 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
self.assertEqual(expected_disk_path_mapping,
resulted_disk_path_mapping)
def test_group_block_devices_by_type(self):
block_device_map = get_fake_block_dev_info()['block_device_mapping']
block_device_map[0]['connection_info'] = {
'driver_volume_type': 'iscsi'}
result = self._volumeops._group_block_devices_by_type(
block_device_map)
expected = {'iscsi': [block_device_map[0]]}
self.assertEqual(expected, result)
def test_parse_disk_qos_specs_using_iops(self):
fake_qos_specs = {
'total_iops_sec': 10,
@ -323,8 +320,7 @@ class VolumeOpsTestCase(test_base.HyperVBaseTestCase):
resulted_disk_path = self._volumeops.get_disk_resource_path(
fake_conn_info)
mock_get_volume_driver.assert_called_once_with(
connection_info=fake_conn_info)
mock_get_volume_driver.assert_called_once_with(fake_conn_info)
fake_volume_driver.get_disk_resource_path.assert_called_once_with(
fake_conn_info)
self.assertEqual(
@ -440,18 +436,52 @@ class BaseVolumeDriverTestCase(test_base.HyperVBaseTestCase):
mock.sentinel.instance_name,
'fake bus')
def test_check_device_paths_multiple_found(self):
device_paths = [mock.sentinel.dev_path_0, mock.sentinel.dev_path_1]
self.assertRaises(exception.InvalidDevicePath,
self._base_vol_driver._check_device_paths,
device_paths)
def test_check_device_paths_none_found(self):
self.assertRaises(exception.DiskNotFound,
self._base_vol_driver._check_device_paths,
[])
def test_check_device_paths_one_device_found(self):
self._base_vol_driver._check_device_paths([mock.sentinel.dev_path])
def test_get_mounted_disk_by_dev_name(self):
vmutils = self._base_vol_driver._vmutils
diskutils = self._base_vol_driver._diskutils
mock_get_dev_number = diskutils.get_device_number_from_device_name
mock_get_dev_number.return_value = mock.sentinel.dev_number
vmutils.get_mounted_disk_by_drive_number.return_value = (
mock.sentinel.disk_path)
disk_path = self._base_vol_driver._get_mounted_disk_path_by_dev_name(
mock.sentinel.dev_name)
mock_get_dev_number.assert_called_once_with(mock.sentinel.dev_name)
vmutils.get_mounted_disk_by_drive_number.assert_called_once_with(
mock.sentinel.dev_number)
self.assertEqual(mock.sentinel.disk_path, disk_path)
@ddt.ddt
class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
"""Unit tests for Hyper-V ISCSIVolumeDriver class."""
def setUp(self):
super(ISCSIVolumeDriverTestCase, self).setUp()
self._volume_driver = volumeops.ISCSIVolumeDriver()
self._volume_driver._vmutils = mock.MagicMock()
self._volume_driver._volutils = mock.MagicMock()
self._iscsi_utils = self._volume_driver._iscsi_utils
self._diskutils = self._volume_driver._diskutils
def _test_get_volume_connector_props(self, initiator_present=True):
expected_props = dict(initiator=self._volume_driver._initiator)
expected_initiator = self._volume_driver._initiator_node_name
expected_props = dict(initiator=expected_initiator)
resulted_props = self._volume_driver.get_volume_connector_props()
self.assertEqual(expected_props, resulted_props)
@ -461,149 +491,163 @@ class ISCSIVolumeDriverTestCase(test_base.HyperVBaseTestCase):
def test_get_vol_connector_props_without_initiator(self):
self._test_get_volume_connector_props(initiator_present=False)
def test_login_storage_target_auth_exception(self):
connection_info = get_fake_connection_info(
auth_method='fake_auth_method')
@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]})
@ddt.unpack
def test_validate_initiators(self, requested_initiators,
available_initiators):
self.flags(iscsi_initiator_list=requested_initiators, group='hyperv')
self._iscsi_utils.get_iscsi_initiators.return_value = (
available_initiators)
expected_valid_initiator = not (
set(requested_initiators).difference(set(available_initiators)))
valid_initiator = self._volume_driver.validate_initiators()
self.assertEqual(expected_valid_initiator, valid_initiator)
def test_get_all_targets_multipath(self):
conn_props = {'target_portals': [mock.sentinel.portal0,
mock.sentinel.portal1],
'target_iqns': [mock.sentinel.target0,
mock.sentinel.target1],
'target_luns': [mock.sentinel.lun0,
mock.sentinel.lun1]}
expected_targets = zip(conn_props['target_portals'],
conn_props['target_iqns'],
conn_props['target_luns'])
resulted_targets = self._volume_driver._get_all_targets(conn_props)
self.assertEqual(list(expected_targets), list(resulted_targets))
def test_get_all_targets_single_path(self):
conn_props = dict(target_portal=mock.sentinel.portal,
target_iqn=mock.sentinel.target,
target_lun=mock.sentinel.lun)
expected_targets = [
(mock.sentinel.portal, mock.sentinel.target, mock.sentinel.lun)]
resulted_targets = self._volume_driver._get_all_targets(conn_props)
self.assertEqual(expected_targets, resulted_targets)
@ddt.data([mock.sentinel.initiator_1, mock.sentinel.initiator_2], [])
@mock.patch.object(volumeops.ISCSIVolumeDriver, '_get_all_targets')
def test_get_all_paths(self, requested_initiators, mock_get_all_targets):
self.flags(iscsi_initiator_list=requested_initiators, group='hyperv')
target = (mock.sentinel.portal, mock.sentinel.target,
mock.sentinel.lun)
mock_get_all_targets.return_value = [target]
paths = self._volume_driver._get_all_paths(mock.sentinel.conn_props)
expected_initiators = requested_initiators or [None]
expected_paths = [(initiator, ) + target
for initiator in expected_initiators]
self.assertEqual(expected_paths, paths)
mock_get_all_targets.assert_called_once_with(mock.sentinel.conn_props)
@ddt.data(True, False)
@mock.patch.object(volumeops.ISCSIVolumeDriver, '_get_all_paths')
def test_connect_volume(self, use_multipath,
mock_get_all_paths):
self.flags(use_multipath_io=use_multipath, group='hyperv')
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, None, None]
conn_info = get_fake_connection_info()
conn_props = conn_info['data']
self._volume_driver.connect_volume(conn_info)
mock_get_all_paths.assert_called_once_with(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=conn_props['auth_username'],
auth_password=conn_props['auth_password'],
mpio_enabled=use_multipath,
initiator_name=mock.sentinel.initiator_name)] *
expected_login_attempts)
@mock.patch.object(volumeops.ISCSIVolumeDriver, '_get_all_paths')
def test_connect_volume_failed(self, mock_get_all_paths):
self.flags(use_multipath_io=True, group='hyperv')
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.assertRaises(exception.VolumeAttachFailed,
self._volume_driver.connect_volume,
get_fake_connection_info())
def test_connect_volume_invalid_auth_method(self):
conn_info = get_fake_connection_info(auth_method='fake_auth')
self.assertRaises(exception.UnsupportedBDMVolumeAuthMethod,
self._volume_driver.login_storage_target,
connection_info)
self._volume_driver.connect_volume,
conn_info)
@mock.patch.object(volumeops.ISCSIVolumeDriver, '_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]
conn_info = get_fake_connection_info()
self._volume_driver.disconnect_volume(conn_info)
self._diskutils.rescan_disks.assert_called_once_with()
mock_get_all_targets.assert_called_once_with(conn_info['data'])
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(volumeops.ISCSIVolumeDriver, '_get_all_targets')
@mock.patch.object(volumeops.ISCSIVolumeDriver, '_check_device_paths')
@mock.patch.object(volumeops.ISCSIVolumeDriver,
'_get_mounted_disk_from_lun')
def _check_login_storage_target(self, mock_get_mounted_disk_from_lun,
dev_number):
connection_info = get_fake_connection_info()
login_target = self._volume_driver._volutils.login_storage_target
get_number = self._volume_driver._volutils.get_device_number_for_target
get_number.return_value = dev_number
'_get_mounted_disk_path_by_dev_name')
def test_get_disk_resource_path(self, mock_get_mounted_disk,
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)]
self._volume_driver.login_storage_target(connection_info)
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]
get_number.assert_called_once_with(mock.sentinel.fake_iqn,
mock.sentinel.fake_lun)
if not dev_number:
login_target.assert_called_once_with(
mock.sentinel.fake_lun, mock.sentinel.fake_iqn,
mock.sentinel.fake_portal, mock.sentinel.fake_user,
mock.sentinel.fake_pass)
mock_get_mounted_disk_from_lun.assert_called_once_with(
mock.sentinel.fake_iqn, mock.sentinel.fake_lun, True)
else:
self.assertFalse(login_target.called)
conn_info = get_fake_connection_info()
volume_paths = self._volume_driver.get_disk_resource_path(conn_info)
self.assertEqual(mock_get_mounted_disk.return_value, volume_paths)
def test_login_storage_target_already_logged(self):
self._check_login_storage_target(dev_number=1)
def test_login_storage_target(self):
self._check_login_storage_target(dev_number=0)
def _check_logout_storage_target(self, disconnected_luns_count=0):
self._volume_driver._volutils.get_target_lun_count.return_value = 1
self._volume_driver.logout_storage_target(
target_iqn=mock.sentinel.fake_iqn,
disconnected_luns_count=disconnected_luns_count)
logout_storage = self._volume_driver._volutils.logout_storage_target
if disconnected_luns_count:
logout_storage.assert_called_once_with(mock.sentinel.fake_iqn)
else:
self.assertFalse(logout_storage.called)
def test_logout_storage_target_skip(self):
self._check_logout_storage_target()
def test_logout_storage_target(self):
self._check_logout_storage_target(disconnected_luns_count=1)
@mock.patch.object(volumeops.ISCSIVolumeDriver,
'_get_mounted_disk_from_lun')
def test_get_disk_resource_path(self, mock_get_mounted_disk_from_lun):
connection_info = get_fake_connection_info()
resulted_disk_path = self._volume_driver.get_disk_resource_path(
connection_info)
mock_get_mounted_disk_from_lun.assert_called_once_with(
connection_info['data']['target_iqn'],
connection_info['data']['target_lun'],
wait_for_device=True)
self.assertEqual(mock_get_mounted_disk_from_lun.return_value,
resulted_disk_path)
def test_get_mounted_disk_from_lun(self):
mock_get_device_number_for_target = (
self._volume_driver._volutils.get_device_number_for_target)
mock_get_device_number_for_target.return_value = 0
mock_get_mounted_disk = (
self._volume_driver._vmutils.get_mounted_disk_by_drive_number)
mock_get_mounted_disk.return_value = mock.sentinel.disk_path
disk = self._volume_driver._get_mounted_disk_from_lun(
mock.sentinel.target_iqn,
mock.sentinel.target_lun)
self.assertEqual(mock.sentinel.disk_path, disk)
def test_get_target_from_disk_path(self):
result = self._volume_driver.get_target_from_disk_path(
mock.sentinel.physical_drive_path)
mock_get_target = (
self._volume_driver._volutils.get_target_from_disk_path)
mock_get_target.assert_called_once_with(
mock.sentinel.physical_drive_path)
self.assertEqual(mock_get_target.return_value, result)
@mock.patch('time.sleep')
def test_get_mounted_disk_from_lun_failure(self, fake_sleep):
self.flags(mounted_disk_query_retry_count=1, group='hyperv')
with mock.patch.object(self._volume_driver._volutils,
'get_device_number_for_target') as m_device_num:
m_device_num.side_effect = [None, -1]
self.assertRaises(exception.NotFound,
self._volume_driver._get_mounted_disk_from_lun,
mock.sentinel.target_iqn,
mock.sentinel.target_lun)
@mock.patch.object(volumeops.ISCSIVolumeDriver, 'logout_storage_target')
def test_disconnect_volumes(self, mock_logout_storage_target):
block_device_info = get_fake_block_dev_info()
connection_info = get_fake_connection_info()
block_device_mapping = block_device_info['block_device_mapping']
block_device_mapping[0]['connection_info'] = connection_info
self._volume_driver.disconnect_volumes(block_device_mapping)
mock_logout_storage_target.assert_called_once_with(
mock.sentinel.fake_iqn, 1)
@mock.patch.object(volumeops.ISCSIVolumeDriver, 'logout_storage_target')
def test_disconnect_volume(self, mock_logout_storage_target):
connection_info = get_fake_connection_info()
self._volume_driver.disconnect_volume(connection_info)
mock_logout_storage_target.assert_called_once_with(
mock.sentinel.fake_iqn)
def test_get_target_lun_count(self):
result = self._volume_driver.get_target_lun_count(
mock.sentinel.target_iqn)
mock_get_lun_count = self._volume_driver._volutils.get_target_lun_count
mock_get_lun_count.assert_called_once_with(mock.sentinel.target_iqn)
self.assertEqual(mock_get_lun_count.return_value, result)
@mock.patch.object(volumeops.ISCSIVolumeDriver, 'login_storage_target')
def test_connect_volume(self, mock_login_storage_target):
self._volume_driver.connect_volume(
mock.sentinel.connection_info)
mock_login_storage_target.assert_called_once_with(
mock.sentinel.connection_info)
mock_get_all_targets.assert_called_once_with(conn_info['data'])
self._iscsi_utils.get_device_number_and_path.assert_has_calls(
[mock.call(mock.sentinel.tg_0, mock.sentinel.lun_0),
mock.call(mock.sentinel.tg_1, mock.sentinel.lun_1)])
mock_check_dev_paths.assert_called_once_with(
set([mock.sentinel.dev_path]))
mock_get_mounted_disk.assert_called_once_with(mock.sentinel.dev_path)
class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase):
@ -684,15 +728,6 @@ class SMBFSVolumeDriverTestCase(test_base.HyperVBaseTestCase):
def test_ensure_already_mounted(self):
self._test_ensure_mounted(is_mounted=True)
def test_disconnect_volumes(self):
mock_unmount_smb_share = (
self._volume_driver._smbutils.unmount_smb_share)
block_device_mapping = [
{'connection_info': self._FAKE_CONNECTION_INFO}]
self._volume_driver.disconnect_volumes(block_device_mapping)
mock_unmount_smb_share.assert_called_once_with(
self._FAKE_SHARE_NORMALIZED)
@mock.patch.object(volumeops.SMBFSVolumeDriver, '_get_disk_path')
def test_set_disk_qos_specs(self, mock_get_disk_path):
self._volume_driver.set_disk_qos_specs(mock.sentinel.connection_info,
@ -765,7 +800,9 @@ class FCVolumeDriverTestCase(test_base.HyperVBaseTestCase):
@mock.patch.object(volumeops.FCVolumeDriver,
'_get_mounted_disk_path_by_dev_name')
@mock.patch.object(volumeops.FCVolumeDriver, '_get_fc_volume_mappings')
def _test_get_disk_resource_path(self, mock_get_fc_mappings,
@mock.patch.object(volumeops.FCVolumeDriver, '_check_device_paths')
def _test_get_disk_resource_path(self, mock_check_dev_paths,
mock_get_fc_mappings,
mock_get_disk_path_by_dev,
fc_mappings_side_effect,
expected_rescan_count,
@ -777,6 +814,8 @@ class FCVolumeDriverTestCase(test_base.HyperVBaseTestCase):
disk_path = self._fc_driver.get_disk_resource_path(
mock.sentinel.conn_info)
self.assertEqual(mock.sentinel.disk_path, disk_path)
mock_check_dev_paths.assert_called_once_with(
set([retrieved_dev_name]))
mock_get_disk_path_by_dev.assert_called_once_with(
retrieved_dev_name)
else:
@ -807,23 +846,6 @@ class FCVolumeDriverTestCase(test_base.HyperVBaseTestCase):
expected_rescan_count=0,
retrieved_dev_name=dev_name)
def test_get_mounted_disk_by_dev_name(self):
vmutils = self._fc_driver._vmutils
vmutils.get_device_number_from_device_name.return_value = (
mock.sentinel.dev_number)
vmutils.get_mounted_disk_by_drive_number.return_value = (
mock.sentinel.disk_path)
disk_path = self._fc_driver._get_mounted_disk_path_by_dev_name(
mock.sentinel.dev_name)
vmutils.get_device_number_from_device_name.assert_called_once_with(
mock.sentinel.dev_name)
vmutils.get_mounted_disk_by_drive_number.assert_called_once_with(
mock.sentinel.dev_number)
self.assertEqual(mock.sentinel.disk_path, disk_path)
@mock.patch.object(volumeops.FCVolumeDriver, '_get_fc_hba_mapping')
def test_get_fc_volume_mappings(self, mock_get_fc_hba_mapping):
fake_target_wwpn = 'FAKE_TARGET_WWPN'

View File

@ -5,6 +5,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
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0