Brick os-brick up to par with cinder brick
This brick's brick up to the same codebase as cinder brick's patch number: ca97a7461f0f56835a0f2dceb9afaebac756f7c3 "Enhance iSCSI multipath support"
This commit is contained in:
parent
88afc6c17e
commit
4f4ced7d8f
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
@ -32,12 +33,35 @@ from os_brick.openstack.common import loopingcall
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
synchronized = lockutils.synchronized_with_prefix('brick-')
|
||||
synchronized = lockutils.synchronized_with_prefix('os-brick-')
|
||||
DEVICE_SCAN_ATTEMPTS_DEFAULT = 3
|
||||
|
||||
|
||||
def get_connector_properties(root_helper, my_ip):
|
||||
"""Get the connection properties for all protocols."""
|
||||
def _check_multipathd_running(root_helper, enforce_multipath):
|
||||
try:
|
||||
putils.execute('multipathd', 'show', 'status',
|
||||
run_as_root=True, root_helper=root_helper)
|
||||
except putils.ProcessExecutionError as err:
|
||||
LOG.error(_LE('multipathd is not running: exit code %(err)s'),
|
||||
{'err': err.exit_code})
|
||||
if enforce_multipath:
|
||||
raise
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_connector_properties(root_helper, my_ip, multipath, enforce_multipath):
|
||||
"""Get the connection properties for all protocols.
|
||||
|
||||
When the connector wants to use multipath, multipath=True should be
|
||||
specified. If enforce_multipath=True is specified too, an exception is
|
||||
thrown when multipathd is not running. Otherwise, it falls back to
|
||||
multipath=False and only the first path shown up is used.
|
||||
For the compatibility reason, even if multipath=False is specified,
|
||||
some cinder storage drivers may export the target for multipath, which
|
||||
can be found via sendtargets discovery.
|
||||
"""
|
||||
|
||||
iscsi = ISCSIConnector(root_helper=root_helper)
|
||||
fc = linuxfc.LinuxFibreChannel(root_helper=root_helper)
|
||||
@ -54,7 +78,9 @@ def get_connector_properties(root_helper, my_ip):
|
||||
wwnns = fc.get_fc_wwnns()
|
||||
if wwnns:
|
||||
props['wwnns'] = wwnns
|
||||
|
||||
props['multipath'] = (multipath and
|
||||
_check_multipathd_running(root_helper,
|
||||
enforce_multipath))
|
||||
return props
|
||||
|
||||
|
||||
@ -191,65 +217,97 @@ class ISCSIConnector(InitiatorConnector):
|
||||
super(ISCSIConnector, self).set_execute(execute)
|
||||
self._linuxscsi.set_execute(execute)
|
||||
|
||||
def _iterate_multiple_targets(self, connection_properties, ips_iqns_luns):
|
||||
for ip, iqn, lun in ips_iqns_luns:
|
||||
props = copy.deepcopy(connection_properties)
|
||||
props['target_portal'] = ip
|
||||
props['target_iqn'] = iqn
|
||||
props['target_lun'] = lun
|
||||
yield props
|
||||
|
||||
def _multipath_targets(self, connection_properties):
|
||||
return zip(connection_properties.get('target_portals', []),
|
||||
connection_properties.get('target_iqns', []),
|
||||
connection_properties.get('target_luns', []))
|
||||
|
||||
def _discover_iscsi_portals(self, connection_properties):
|
||||
if all([key in connection_properties for key in ('target_portals',
|
||||
'target_iqns')]):
|
||||
# Use targets specified by connection_properties
|
||||
return zip(connection_properties['target_portals'],
|
||||
connection_properties['target_iqns'])
|
||||
|
||||
# Discover and return every available target
|
||||
out = self._run_iscsiadm_bare(['-m',
|
||||
'discovery',
|
||||
'-t',
|
||||
'sendtargets',
|
||||
'-p',
|
||||
connection_properties['target_portal']],
|
||||
check_exit_code=[0, 255])[0] \
|
||||
or ""
|
||||
|
||||
return self._get_target_portals_from_iscsiadm_output(out)
|
||||
|
||||
@synchronized('connect_volume')
|
||||
def connect_volume(self, connection_properties):
|
||||
"""Attach the volume to instance_name.
|
||||
|
||||
connection_properties for iSCSI must include:
|
||||
target_portal - ip and optional port
|
||||
target_iqn - iSCSI Qualified Name
|
||||
target_lun - LUN id of the volume
|
||||
target_portal(s) - ip and optional port
|
||||
target_iqn(s) - iSCSI Qualified Name
|
||||
target_lun(s) - LUN id of the volume
|
||||
Note that plural keys may be used when use_multipath=True
|
||||
"""
|
||||
|
||||
device_info = {'type': 'block'}
|
||||
|
||||
if self.use_multipath:
|
||||
#multipath installed, discovering other targets if available
|
||||
target_portal = connection_properties['target_portal']
|
||||
out = self._run_iscsiadm_bare(['-m',
|
||||
'discovery',
|
||||
'-t',
|
||||
'sendtargets',
|
||||
'-p',
|
||||
target_portal],
|
||||
check_exit_code=[0, 255])[0] \
|
||||
or ""
|
||||
|
||||
for ip, iqn in self._get_target_portals_from_iscsiadm_output(out):
|
||||
props = connection_properties.copy()
|
||||
for ip, iqn in self._discover_iscsi_portals(connection_properties):
|
||||
props = copy.deepcopy(connection_properties)
|
||||
props['target_portal'] = ip
|
||||
props['target_iqn'] = iqn
|
||||
self._connect_to_iscsi_portal(props)
|
||||
|
||||
self._rescan_iscsi()
|
||||
host_devices = self._get_device_path(connection_properties)
|
||||
else:
|
||||
self._connect_to_iscsi_portal(connection_properties)
|
||||
|
||||
host_device = self._get_device_path(connection_properties)
|
||||
host_devices = self._get_device_path(connection_properties)
|
||||
|
||||
# The /dev/disk/by-path/... node is not always present immediately
|
||||
# TODO(justinsb): This retry-with-delay is a pattern, move to utils?
|
||||
tries = 0
|
||||
while not os.path.exists(host_device):
|
||||
# Loop until at least 1 path becomes available
|
||||
while all(map(lambda x: not os.path.exists(x), host_devices)):
|
||||
if tries >= self.device_scan_attempts:
|
||||
raise exception.VolumeDeviceNotFound(device=host_device)
|
||||
raise exception.VolumeDeviceNotFound(device=host_devices)
|
||||
|
||||
LOG.warn(_LW("ISCSI volume not yet found at: %(host_device)s. "
|
||||
LOG.warn(_LW("ISCSI volume not yet found at: %(host_devices)s. "
|
||||
"Will rescan & retry. Try number: %(tries)s"),
|
||||
{'host_device': host_device,
|
||||
{'host_devices': host_devices,
|
||||
'tries': tries})
|
||||
|
||||
# The rescan isn't documented as being necessary(?), but it helps
|
||||
self._run_iscsiadm(connection_properties, ("--rescan",))
|
||||
if self.use_multipath:
|
||||
self._rescan_iscsi()
|
||||
else:
|
||||
self._run_iscsiadm(connection_properties, ("--rescan",))
|
||||
|
||||
tries = tries + 1
|
||||
if not os.path.exists(host_device):
|
||||
if all(map(lambda x: not os.path.exists(x), host_devices)):
|
||||
time.sleep(tries ** 2)
|
||||
else:
|
||||
break
|
||||
|
||||
if tries != 0:
|
||||
LOG.debug("Found iSCSI node %(host_device)s "
|
||||
LOG.debug("Found iSCSI node %(host_devices)s "
|
||||
"(after %(tries)s rescans)",
|
||||
{'host_device': host_device, 'tries': tries})
|
||||
{'host_devices': host_devices, 'tries': tries})
|
||||
|
||||
# Choose an accessible host device
|
||||
host_device = next(dev for dev in host_devices if os.path.exists(dev))
|
||||
|
||||
if self.use_multipath:
|
||||
#we use the multipath device instead of the single path device
|
||||
@ -266,9 +324,9 @@ class ISCSIConnector(InitiatorConnector):
|
||||
"""Detach the volume from instance_name.
|
||||
|
||||
connection_properties for iSCSI must include:
|
||||
target_portal - IP and optional port
|
||||
target_iqn - iSCSI Qualified Name
|
||||
target_lun - LUN id of the volume
|
||||
target_portal(s) - IP and optional port
|
||||
target_iqn(s) - iSCSI Qualified Name
|
||||
target_lun(s) - LUN id of the volume
|
||||
"""
|
||||
# Moved _rescan_iscsi and _rescan_multipath
|
||||
# from _disconnect_volume_multipath_iscsi to here.
|
||||
@ -276,19 +334,43 @@ class ISCSIConnector(InitiatorConnector):
|
||||
# but before logging out, the removed devices under /dev/disk/by-path
|
||||
# will reappear after rescan.
|
||||
self._rescan_iscsi()
|
||||
host_device = self._get_device_path(connection_properties)
|
||||
multipath_device = None
|
||||
if self.use_multipath:
|
||||
self._rescan_multipath()
|
||||
multipath_device = self._get_multipath_device_name(host_device)
|
||||
host_device = multipath_device = None
|
||||
host_devices = self._get_device_path(connection_properties)
|
||||
# Choose an accessible host device
|
||||
for dev in host_devices:
|
||||
if os.path.exists(dev):
|
||||
host_device = dev
|
||||
multipath_device = self._get_multipath_device_name(dev)
|
||||
if multipath_device:
|
||||
break
|
||||
if not host_device:
|
||||
LOG.error(_LE("No accessible volume device: %(host_devices)s"),
|
||||
{'host_devices': host_devices})
|
||||
raise exception.VolumeDeviceNotFound(device=host_devices)
|
||||
|
||||
if multipath_device:
|
||||
device_realpath = os.path.realpath(host_device)
|
||||
self._linuxscsi.remove_multipath_device(device_realpath)
|
||||
return self._disconnect_volume_multipath_iscsi(
|
||||
connection_properties, multipath_device)
|
||||
|
||||
# When multiple portals/iqns/luns are specified, we need to remove
|
||||
# unused devices created by logging into other LUNs' session.
|
||||
ips_iqns_luns = self._multipath_targets(connection_properties)
|
||||
if not ips_iqns_luns:
|
||||
ips_iqns_luns = [[connection_properties['target_portal'],
|
||||
connection_properties['target_iqn'],
|
||||
connection_properties.get('target_lun', 0)]]
|
||||
for props in self._iterate_multiple_targets(connection_properties,
|
||||
ips_iqns_luns):
|
||||
self._disconnect_volume_iscsi(props)
|
||||
|
||||
def _disconnect_volume_iscsi(self, connection_properties):
|
||||
# remove the device from the scsi subsystem
|
||||
# this eliminates any stale entries until logout
|
||||
host_device = self._get_device_path(connection_properties)[0]
|
||||
dev_name = self._linuxscsi.get_name_from_path(host_device)
|
||||
if dev_name:
|
||||
self._linuxscsi.remove_scsi_device(dev_name)
|
||||
@ -306,11 +388,16 @@ class ISCSIConnector(InitiatorConnector):
|
||||
self._disconnect_from_iscsi_portal(connection_properties)
|
||||
|
||||
def _get_device_path(self, connection_properties):
|
||||
multipath_targets = self._multipath_targets(connection_properties)
|
||||
if multipath_targets:
|
||||
return ["/dev/disk/by-path/ip-%s-iscsi-%s-lun-%s" % x for x in
|
||||
multipath_targets]
|
||||
|
||||
path = ("/dev/disk/by-path/ip-%(portal)s-iscsi-%(iqn)s-lun-%(lun)s" %
|
||||
{'portal': connection_properties['target_portal'],
|
||||
'iqn': connection_properties['target_iqn'],
|
||||
'lun': connection_properties.get('target_lun', 0)})
|
||||
return path
|
||||
return [path]
|
||||
|
||||
def get_initiator(self):
|
||||
"""Secure helper to read file as root."""
|
||||
@ -370,16 +457,7 @@ class ISCSIConnector(InitiatorConnector):
|
||||
# Do a discovery to find all targets.
|
||||
# Targets for multiple paths for the same multipath device
|
||||
# may not be the same.
|
||||
out = self._run_iscsiadm_bare(['-m',
|
||||
'discovery',
|
||||
'-t',
|
||||
'sendtargets',
|
||||
'-p',
|
||||
connection_properties['target_portal']],
|
||||
check_exit_code=[0, 255])[0] \
|
||||
or ""
|
||||
|
||||
ips_iqns = self._get_target_portals_from_iscsiadm_output(out)
|
||||
ips_iqns = self._discover_iscsi_portals(connection_properties)
|
||||
|
||||
if not devices:
|
||||
# disconnect if no other multipath devices
|
||||
@ -499,7 +577,7 @@ class ISCSIConnector(InitiatorConnector):
|
||||
|
||||
def _disconnect_mpath(self, connection_properties, ips_iqns):
|
||||
for ip, iqn in ips_iqns:
|
||||
props = connection_properties.copy()
|
||||
props = copy.deepcopy(connection_properties)
|
||||
props['target_portal'] = ip
|
||||
props['target_iqn'] = iqn
|
||||
self._disconnect_from_iscsi_portal(props)
|
||||
|
@ -13,7 +13,9 @@
|
||||
# under the License.
|
||||
|
||||
import os.path
|
||||
import socket
|
||||
import string
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import mock
|
||||
@ -21,16 +23,68 @@ from oslo_concurrency import processutils as putils
|
||||
import testtools
|
||||
|
||||
from os_brick import exception
|
||||
from os_brick.i18n import _
|
||||
from os_brick.i18n import _LE
|
||||
from os_brick.initiator import connector
|
||||
from os_brick.initiator import host_driver
|
||||
from os_brick.initiator import linuxfc
|
||||
from os_brick.openstack.common import log as logging
|
||||
from os_brick.openstack.common import loopingcall
|
||||
from os_brick.tests import base
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
MY_IP = '10.0.0.1'
|
||||
|
||||
|
||||
class ConnectorUtilsTestCase(base.TestCase):
|
||||
|
||||
@mock.patch.object(socket, 'gethostname', return_value='fakehost')
|
||||
@mock.patch.object(connector.ISCSIConnector, 'get_initiator',
|
||||
return_value='fakeinitiator')
|
||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_wwpns',
|
||||
return_value=None)
|
||||
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_wwnns',
|
||||
return_value=None)
|
||||
def _test_brick_get_connector_properties(self, multipath,
|
||||
enforce_multipath,
|
||||
multipath_result,
|
||||
mock_wwnns, mock_wwpns,
|
||||
mock_initiator, mock_gethostname):
|
||||
props_actual = connector.get_connector_properties('sudo',
|
||||
MY_IP,
|
||||
multipath,
|
||||
enforce_multipath)
|
||||
props = {'initiator': 'fakeinitiator',
|
||||
'host': 'fakehost',
|
||||
'ip': MY_IP,
|
||||
'multipath': multipath_result}
|
||||
self.assertEqual(props, props_actual)
|
||||
|
||||
def test_brick_get_connector_properties(self):
|
||||
self._test_brick_get_connector_properties(False, False, False)
|
||||
|
||||
@mock.patch.object(putils, 'execute')
|
||||
def test_brick_get_connector_properties_multipath(self, mock_execute):
|
||||
self._test_brick_get_connector_properties(True, True, True)
|
||||
mock_execute.assert_called_once_with('multipathd', 'show', 'status',
|
||||
run_as_root=True,
|
||||
root_helper='sudo')
|
||||
|
||||
@mock.patch.object(putils, 'execute',
|
||||
side_effect=putils.ProcessExecutionError)
|
||||
def test_brick_get_connector_properties_fallback(self, mock_execute):
|
||||
self._test_brick_get_connector_properties(True, False, False)
|
||||
mock_execute.assert_called_once_with('multipathd', 'show', 'status',
|
||||
run_as_root=True,
|
||||
root_helper='sudo')
|
||||
|
||||
@mock.patch.object(putils, 'execute',
|
||||
side_effect=putils.ProcessExecutionError)
|
||||
def test_brick_get_connector_properties_raise(self, mock_execute):
|
||||
self.assertRaises(putils.ProcessExecutionError,
|
||||
self._test_brick_get_connector_properties,
|
||||
True, True, None)
|
||||
|
||||
|
||||
class ConnectorTestCase(base.TestCase):
|
||||
|
||||
@ -123,6 +177,8 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
|
||||
super(ISCSIConnectorTestCase, self).setUp()
|
||||
self.connector = connector.ISCSIConnector(
|
||||
None, execute=self.fake_execute, use_multipath=False)
|
||||
self.connector_with_multipath = connector.ISCSIConnector(
|
||||
None, execute=self.fake_execute, use_multipath=True)
|
||||
|
||||
get_name_mock = mock.Mock()
|
||||
get_name_mock.return_value = "/dev/sdb"
|
||||
@ -139,6 +195,17 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
|
||||
}
|
||||
}
|
||||
|
||||
def iscsi_connection_multipath(self, volume, locations, iqns, luns):
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': {
|
||||
'volume_id': volume['id'],
|
||||
'target_portals': locations,
|
||||
'target_iqns': iqns,
|
||||
'target_luns': luns,
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_initiator(self):
|
||||
def initiator_no_file(*args, **kwargs):
|
||||
raise putils.ProcessExecutionError('No file')
|
||||
@ -166,7 +233,9 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
|
||||
@testtools.skipUnless(os.path.exists('/dev/disk/by-path'),
|
||||
'Test requires /dev/disk/by-path')
|
||||
def test_connect_volume(self):
|
||||
self.stubs.Set(os.path, 'exists', lambda x: True)
|
||||
exists_mock = mock.Mock()
|
||||
exists_mock.return_value = True
|
||||
os.path.exists = exists_mock
|
||||
location = '10.0.2.15:3260'
|
||||
name = 'volume-00000001'
|
||||
iqn = 'iqn.2010-10.org.openstack:%s' % name
|
||||
@ -234,18 +303,129 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
|
||||
exists_mock = mock.Mock()
|
||||
exists_mock.return_value = True
|
||||
os.path.exists = exists_mock
|
||||
|
||||
result = self.connector_with_multipath.connect_volume(
|
||||
connection_properties['data'])
|
||||
expected_result = {'path': 'iqn.2010-10.org.openstack:volume-00000001',
|
||||
'type': 'block'}
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
@mock.patch.object(os.path, 'exists', return_value=True)
|
||||
@mock.patch.object(host_driver.HostDriver, 'get_all_block_devices')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_rescan_multipath')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_run_multipath')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_name')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_iqn')
|
||||
def test_connect_volume_with_multiple_portals(
|
||||
self, mock_get_iqn, mock_device_name, mock_run_multipath,
|
||||
mock_rescan_multipath, mock_devices, mock_exists):
|
||||
location1 = '10.0.2.15:3260'
|
||||
location2 = '10.0.3.15:3260'
|
||||
name1 = 'volume-00000001-1'
|
||||
name2 = 'volume-00000001-2'
|
||||
iqn1 = 'iqn.2010-10.org.openstack:%s' % name1
|
||||
iqn2 = 'iqn.2010-10.org.openstack:%s' % name2
|
||||
fake_multipath_dev = '/dev/mapper/fake-multipath-dev'
|
||||
vol = {'id': 1, 'name': name1}
|
||||
connection_properties = self.iscsi_connection_multipath(
|
||||
vol, [location1, location2], [iqn1, iqn2], [1, 2])
|
||||
devs = ['/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1),
|
||||
'/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (location2, iqn2)]
|
||||
mock_devices.return_value = devs
|
||||
mock_device_name.return_value = fake_multipath_dev
|
||||
mock_get_iqn.return_value = [iqn1, iqn2]
|
||||
|
||||
result = self.connector_with_multipath.connect_volume(
|
||||
connection_properties['data'])
|
||||
expected_result = {'path': fake_multipath_dev, 'type': 'block'}
|
||||
cmd_format = 'iscsiadm -m node -T %s -p %s --%s'
|
||||
expected_commands = [cmd_format % (iqn1, location1, 'login'),
|
||||
cmd_format % (iqn2, location2, 'login')]
|
||||
self.assertEqual(expected_result, result)
|
||||
for command in expected_commands:
|
||||
self.assertIn(command, self.cmds)
|
||||
mock_device_name.assert_called_once_with(devs[0])
|
||||
|
||||
self.cmds = []
|
||||
self.connector_with_multipath.disconnect_volume(
|
||||
connection_properties['data'], result)
|
||||
expected_commands = [cmd_format % (iqn1, location1, 'logout'),
|
||||
cmd_format % (iqn2, location2, 'logout')]
|
||||
for command in expected_commands:
|
||||
self.assertIn(command, self.cmds)
|
||||
|
||||
@mock.patch.object(os.path, 'exists')
|
||||
@mock.patch.object(host_driver.HostDriver, 'get_all_block_devices')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_rescan_multipath')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_run_multipath')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_device_name')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_get_multipath_iqn')
|
||||
@mock.patch.object(connector.ISCSIConnector, '_run_iscsiadm')
|
||||
def test_connect_volume_with_multiple_portals_primary_error(
|
||||
self, mock_iscsiadm, mock_get_iqn, mock_device_name,
|
||||
mock_run_multipath, mock_rescan_multipath, mock_devices,
|
||||
mock_exists):
|
||||
location1 = '10.0.2.15:3260'
|
||||
location2 = '10.0.3.15:3260'
|
||||
name1 = 'volume-00000001-1'
|
||||
name2 = 'volume-00000001-2'
|
||||
iqn1 = 'iqn.2010-10.org.openstack:%s' % name1
|
||||
iqn2 = 'iqn.2010-10.org.openstack:%s' % name2
|
||||
fake_multipath_dev = '/dev/mapper/fake-multipath-dev'
|
||||
vol = {'id': 1, 'name': name1}
|
||||
connection_properties = self.iscsi_connection_multipath(
|
||||
vol, [location1, location2], [iqn1, iqn2], [1, 2])
|
||||
dev1 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-1' % (location1, iqn1)
|
||||
dev2 = '/dev/disk/by-path/ip-%s-iscsi-%s-lun-2' % (location2, iqn2)
|
||||
|
||||
def fake_run_iscsiadm(iscsi_properties, iscsi_command, **kwargs):
|
||||
if iscsi_properties['target_portal'] == location1:
|
||||
if iscsi_command == ('--login',):
|
||||
raise putils.ProcessExecutionError(None, None, 21)
|
||||
return mock.DEFAULT
|
||||
|
||||
mock_exists.side_effect = lambda x: x != dev1
|
||||
mock_devices.return_value = [dev2]
|
||||
mock_device_name.return_value = fake_multipath_dev
|
||||
mock_get_iqn.return_value = [iqn2]
|
||||
mock_iscsiadm.side_effect = fake_run_iscsiadm
|
||||
|
||||
props = connection_properties['data'].copy()
|
||||
result = self.connector_with_multipath.connect_volume(
|
||||
connection_properties['data'])
|
||||
|
||||
expected_result = {'path': fake_multipath_dev, 'type': 'block'}
|
||||
self.assertEqual(expected_result, result)
|
||||
mock_device_name.assert_called_once_with(dev2)
|
||||
props['target_portal'] = location1
|
||||
props['target_iqn'] = iqn1
|
||||
mock_iscsiadm.assert_any_call(props, ('--login',),
|
||||
check_exit_code=[0, 255])
|
||||
props['target_portal'] = location2
|
||||
props['target_iqn'] = iqn2
|
||||
mock_iscsiadm.assert_any_call(props, ('--login',),
|
||||
check_exit_code=[0, 255])
|
||||
|
||||
mock_iscsiadm.reset_mock()
|
||||
self.connector_with_multipath.disconnect_volume(
|
||||
connection_properties['data'], result)
|
||||
|
||||
props = connection_properties['data'].copy()
|
||||
props['target_portal'] = location1
|
||||
props['target_iqn'] = iqn1
|
||||
mock_iscsiadm.assert_any_call(props, ('--logout',),
|
||||
check_exit_code=[0, 21, 255])
|
||||
props['target_portal'] = location2
|
||||
props['target_iqn'] = iqn2
|
||||
mock_iscsiadm.assert_any_call(props, ('--logout',),
|
||||
check_exit_code=[0, 21, 255])
|
||||
|
||||
def test_connect_volume_with_not_found_device(self):
|
||||
exists_mock = mock.Mock()
|
||||
exists_mock.return_value = False
|
||||
os.path.exists = exists_mock
|
||||
sleep_mock = mock.Mock()
|
||||
sleep_mock.return_value = None
|
||||
sleep_mock.return_value = False
|
||||
time.sleep = sleep_mock
|
||||
location = '10.0.2.15:3260'
|
||||
name = 'volume-00000001'
|
||||
@ -313,7 +493,6 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
|
||||
|
||||
def fake_disconnect_from_iscsi_portal(properties):
|
||||
result.append(properties)
|
||||
|
||||
iqn1 = 'iqn.2013-01.ro.com.netapp:node.netapp01'
|
||||
iqn2 = 'iqn.2013-01.ro.com.netapp:node.netapp02'
|
||||
iqns = [iqn1, iqn2]
|
||||
@ -380,6 +559,48 @@ class ISCSIConnectorTestCase(ConnectorTestCase):
|
||||
# Target not in use by other mp devices, disconnect
|
||||
self.assertEqual([fake_property], result)
|
||||
|
||||
def test_disconnect_volume_multipath_iscsi_with_invalid_symlink(self):
|
||||
result = []
|
||||
|
||||
def fake_disconnect_from_iscsi_portal(properties):
|
||||
result.append(properties)
|
||||
|
||||
portal = '10.0.0.1:3260'
|
||||
name = 'volume-00000001'
|
||||
iqn = 'iqn.2010-10.org.openstack:%s' % name
|
||||
dev = ('ip-%s-iscsi-%s-lun-0' % (portal, iqn))
|
||||
|
||||
get_portals_mock = mock.Mock()
|
||||
get_portals_mock.return_value = [[portal, iqn]]
|
||||
self.connector._get_target_portals_from_iscsiadm_output = \
|
||||
get_portals_mock
|
||||
|
||||
rescan_iscsi_mock = mock.Mock()
|
||||
rescan_iscsi_mock.return_value = None
|
||||
self.connector._rescan_iscsi = rescan_iscsi_mock
|
||||
|
||||
rescan_multipath_mock = mock.Mock()
|
||||
rescan_multipath_mock.return_value = None
|
||||
self.connector._rescan_multipath = rescan_multipath_mock
|
||||
|
||||
get_all_devices_mock = mock.Mock()
|
||||
get_all_devices_mock.return_value = [dev, '/dev/mapper/md-1']
|
||||
self.connector.driver.get_all_block_devices = get_all_devices_mock
|
||||
|
||||
self.connector._disconnect_from_iscsi_portal = \
|
||||
fake_disconnect_from_iscsi_portal
|
||||
|
||||
# Simulate a broken symlink by returning False for os.path.exists(dev)
|
||||
mock_exists = mock.Mock()
|
||||
mock_exists.return_value = False
|
||||
os.path.exists = mock_exists
|
||||
fake_property = {'target_portal': portal,
|
||||
'target_iqn': iqn}
|
||||
self.connector._disconnect_volume_multipath_iscsi(fake_property,
|
||||
'fake/multipath')
|
||||
# Target not in use by other mp devices, disconnect
|
||||
self.assertEqual([fake_property], result)
|
||||
|
||||
|
||||
class FibreChannelConnectorTestCase(ConnectorTestCase):
|
||||
def setUp(self):
|
||||
@ -458,6 +679,7 @@ class FibreChannelConnectorTestCase(ConnectorTestCase):
|
||||
get_device_info_mock = mock.Mock()
|
||||
get_device_info_mock.return_value = devices['devices'][0]
|
||||
self.connector._linuxscsi.get_device_info = get_device_info_mock
|
||||
|
||||
location = '10.0.2.15:3260'
|
||||
name = 'volume-00000001'
|
||||
vol = {'id': 1, 'name': name}
|
||||
@ -515,7 +737,7 @@ class FakeFixedIntervalLoopingCall(object):
|
||||
except loopingcall.LoopingCallDone:
|
||||
return self
|
||||
except Exception:
|
||||
LOG.exception(_('in fixed duration looping call'))
|
||||
LOG.exception(_LE('in fixed duration looping call'))
|
||||
raise
|
||||
|
||||
|
||||
@ -569,7 +791,7 @@ class AoEConnectorTestCase(ConnectorTestCase):
|
||||
self.assertDictMatch(volume_info, expected_info)
|
||||
|
||||
def test_connect_volume_could_not_discover_path(self):
|
||||
aoe_device, aoe_path = self.connector._get_aoe_info(
|
||||
_aoe_device, aoe_path = self.connector._get_aoe_info(
|
||||
self.connection_properties)
|
||||
|
||||
exists_mock = mock.Mock()
|
||||
@ -615,7 +837,7 @@ class RemoteFsConnectorTestCase(ConnectorTestCase):
|
||||
client = self.connector._remotefsclient
|
||||
client.mount = mock.Mock()
|
||||
client.get_mount_point = mock.Mock()
|
||||
client.get_mount_point.return_value = "ass"
|
||||
client.get_mount_point.return_value = "something"
|
||||
|
||||
self.connector.connect_volume(self.connection_properties)
|
||||
|
||||
@ -643,3 +865,201 @@ class LocalConnectorTestCase(base.TestCase):
|
||||
cprops = {}
|
||||
self.assertRaises(ValueError,
|
||||
self.connector.connect_volume, cprops)
|
||||
|
||||
|
||||
class HuaweiStorHyperConnectorTestCase(ConnectorTestCase):
|
||||
"""Test cases for StorHyper initiator class."""
|
||||
|
||||
attached = False
|
||||
|
||||
def setUp(self):
|
||||
super(HuaweiStorHyperConnectorTestCase, self).setUp()
|
||||
self.fake_sdscli_file = tempfile.mktemp()
|
||||
self.addCleanup(os.remove, self.fake_sdscli_file)
|
||||
newefile = open(self.fake_sdscli_file, 'w')
|
||||
newefile.write('test')
|
||||
newefile.close()
|
||||
|
||||
self.connector = connector.HuaweiStorHyperConnector(
|
||||
None, execute=self.fake_execute)
|
||||
self.connector.cli_path = self.fake_sdscli_file
|
||||
self.connector.iscliexist = True
|
||||
|
||||
self.connector_fail = connector.HuaweiStorHyperConnector(
|
||||
None, execute=self.fake_execute_fail)
|
||||
self.connector_fail.cli_path = self.fake_sdscli_file
|
||||
self.connector_fail.iscliexist = True
|
||||
|
||||
self.connector_nocli = connector.HuaweiStorHyperConnector(
|
||||
None, execute=self.fake_execute_fail)
|
||||
self.connector_nocli.cli_path = self.fake_sdscli_file
|
||||
self.connector_nocli.iscliexist = False
|
||||
|
||||
self.connection_properties = {
|
||||
'access_mode': 'rw',
|
||||
'qos_specs': None,
|
||||
'volume_id': 'volume-b2911673-863c-4380-a5f2-e1729eecfe3f'
|
||||
}
|
||||
|
||||
self.device_info = {'type': 'block',
|
||||
'path': '/dev/vdxxx'}
|
||||
HuaweiStorHyperConnectorTestCase.attached = False
|
||||
|
||||
def fake_execute(self, *cmd, **kwargs):
|
||||
method = cmd[2]
|
||||
self.cmds.append(string.join(cmd))
|
||||
if 'attach' == method:
|
||||
HuaweiStorHyperConnectorTestCase.attached = True
|
||||
return 'ret_code=0', None
|
||||
if 'querydev' == method:
|
||||
if HuaweiStorHyperConnectorTestCase.attached:
|
||||
return 'ret_code=0\ndev_addr=/dev/vdxxx', None
|
||||
else:
|
||||
return 'ret_code=1\ndev_addr=/dev/vdxxx', None
|
||||
if 'detach' == method:
|
||||
HuaweiStorHyperConnectorTestCase.attached = False
|
||||
return 'ret_code=0', None
|
||||
|
||||
def fake_execute_fail(self, *cmd, **kwargs):
|
||||
method = cmd[2]
|
||||
self.cmds.append(string.join(cmd))
|
||||
if 'attach' == method:
|
||||
HuaweiStorHyperConnectorTestCase.attached = False
|
||||
return 'ret_code=330151401', None
|
||||
if 'querydev' == method:
|
||||
if HuaweiStorHyperConnectorTestCase.attached:
|
||||
return 'ret_code=0\ndev_addr=/dev/vdxxx', None
|
||||
else:
|
||||
return 'ret_code=1\ndev_addr=/dev/vdxxx', None
|
||||
if 'detach' == method:
|
||||
HuaweiStorHyperConnectorTestCase.attached = True
|
||||
return 'ret_code=330155007', None
|
||||
|
||||
def test_connect_volume(self):
|
||||
"""Test the basic connect volume case."""
|
||||
|
||||
retval = self.connector.connect_volume(self.connection_properties)
|
||||
self.assertEqual(self.device_info, retval)
|
||||
|
||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c querydev'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
||||
LOG.debug("self.cmds = %s." % self.cmds)
|
||||
LOG.debug("expected = %s." % expected_commands)
|
||||
|
||||
self.assertEqual(expected_commands, self.cmds)
|
||||
|
||||
def test_disconnect_volume(self):
|
||||
"""Test the basic disconnect volume case."""
|
||||
self.connector.connect_volume(self.connection_properties)
|
||||
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
|
||||
self.connector.disconnect_volume(self.connection_properties,
|
||||
self.device_info)
|
||||
self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached)
|
||||
|
||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c querydev'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c detach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
||||
|
||||
LOG.debug("self.cmds = %s." % self.cmds)
|
||||
LOG.debug("expected = %s." % expected_commands)
|
||||
|
||||
self.assertEqual(expected_commands, self.cmds)
|
||||
|
||||
def test_is_volume_connected(self):
|
||||
"""Test if volume connected to host case."""
|
||||
self.connector.connect_volume(self.connection_properties)
|
||||
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
|
||||
is_connected = self.connector.is_volume_connected(
|
||||
'volume-b2911673-863c-4380-a5f2-e1729eecfe3f')
|
||||
self.assertEqual(HuaweiStorHyperConnectorTestCase.attached,
|
||||
is_connected)
|
||||
self.connector.disconnect_volume(self.connection_properties,
|
||||
self.device_info)
|
||||
self.assertEqual(False, HuaweiStorHyperConnectorTestCase.attached)
|
||||
is_connected = self.connector.is_volume_connected(
|
||||
'volume-b2911673-863c-4380-a5f2-e1729eecfe3f')
|
||||
self.assertEqual(HuaweiStorHyperConnectorTestCase.attached,
|
||||
is_connected)
|
||||
|
||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c querydev'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c querydev'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c detach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c querydev'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
||||
|
||||
LOG.debug("self.cmds = %s." % self.cmds)
|
||||
LOG.debug("expected = %s." % expected_commands)
|
||||
|
||||
self.assertEqual(expected_commands, self.cmds)
|
||||
|
||||
def test__analyze_output(self):
|
||||
cliout = 'ret_code=0\ndev_addr=/dev/vdxxx\nret_desc="success"'
|
||||
analyze_result = {'dev_addr': '/dev/vdxxx',
|
||||
'ret_desc': '"success"',
|
||||
'ret_code': '0'}
|
||||
result = self.connector._analyze_output(cliout)
|
||||
self.assertEqual(analyze_result, result)
|
||||
|
||||
def test_connect_volume_fail(self):
|
||||
"""Test the fail connect volume case."""
|
||||
self.assertRaises(exception.BrickException,
|
||||
self.connector_fail.connect_volume,
|
||||
self.connection_properties)
|
||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
||||
LOG.debug("self.cmds = %s." % self.cmds)
|
||||
LOG.debug("expected = %s." % expected_commands)
|
||||
self.assertEqual(expected_commands, self.cmds)
|
||||
|
||||
def test_disconnect_volume_fail(self):
|
||||
"""Test the fail disconnect volume case."""
|
||||
self.connector.connect_volume(self.connection_properties)
|
||||
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
|
||||
self.assertRaises(exception.BrickException,
|
||||
self.connector_fail.disconnect_volume,
|
||||
self.connection_properties,
|
||||
self.device_info)
|
||||
|
||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c querydev'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c detach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
||||
|
||||
LOG.debug("self.cmds = %s." % self.cmds)
|
||||
LOG.debug("expected = %s." % expected_commands)
|
||||
|
||||
self.assertEqual(expected_commands, self.cmds)
|
||||
|
||||
def test_connect_volume_nocli(self):
|
||||
"""Test the fail connect volume case."""
|
||||
self.assertRaises(exception.BrickException,
|
||||
self.connector_nocli.connect_volume,
|
||||
self.connection_properties)
|
||||
|
||||
def test_disconnect_volume_nocli(self):
|
||||
"""Test the fail disconnect volume case."""
|
||||
self.connector.connect_volume(self.connection_properties)
|
||||
self.assertEqual(True, HuaweiStorHyperConnectorTestCase.attached)
|
||||
self.assertRaises(exception.BrickException,
|
||||
self.connector_nocli.disconnect_volume,
|
||||
self.connection_properties,
|
||||
self.device_info)
|
||||
expected_commands = [self.fake_sdscli_file + ' -c attach'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f',
|
||||
self.fake_sdscli_file + ' -c querydev'
|
||||
' -v volume-b2911673-863c-4380-a5f2-e1729eecfe3f']
|
||||
|
||||
LOG.debug("self.cmds = %s." % self.cmds)
|
||||
LOG.debug("expected = %s." % expected_commands)
|
||||
|
Loading…
x
Reference in New Issue
Block a user