
It was recently added to open-iscsi the functionality to disable automatic LUN scans on iscsid start, on login, and on reception of AEN/AER messages reporting LUN data has changed. Those 3 cases were one of the causes why Nova-CPU and Cinder-Volumes nodes would have unexpected devices. With this new feature we can prevent them from appearing unnexpectedly. This patch adds the mechanism required to configure our sessions for manual scans in a backward compatible way. Manual scans are enabled setting `node.session.scan` to `manual`. Change-Id: I146a74f9f79c68a89677b9b26a324e06a35886f2
1265 lines
60 KiB
Python
1265 lines
60 KiB
Python
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# 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 collections
|
|
import mock
|
|
import os
|
|
|
|
import ddt
|
|
from oslo_concurrency import processutils as putils
|
|
|
|
from os_brick import exception
|
|
from os_brick.initiator.connectors import iscsi
|
|
from os_brick.initiator import linuxscsi
|
|
from os_brick.privileged import rootwrap as priv_rootwrap
|
|
from os_brick.tests.initiator import test_connector
|
|
|
|
|
|
@ddt.ddt
|
|
class ISCSIConnectorTestCase(test_connector.ConnectorTestCase):
|
|
CON_PROPS = {
|
|
'volume_id': 'vol_id',
|
|
'target_portal': 'ip1:port1',
|
|
'target_iqn': 'tgt1',
|
|
'target_lun': 4,
|
|
'target_portals': ['ip1:port1', 'ip2:port2', 'ip3:port3',
|
|
'ip4:port4'],
|
|
'target_iqns': ['tgt1', 'tgt2', 'tgt3', 'tgt4'],
|
|
'target_luns': [4, 5, 6, 7],
|
|
}
|
|
|
|
def setUp(self):
|
|
super(ISCSIConnectorTestCase, self).setUp()
|
|
self.connector = iscsi.ISCSIConnector(
|
|
None, execute=self.fake_execute, use_multipath=False)
|
|
|
|
self.connector_with_multipath = iscsi.ISCSIConnector(
|
|
None, execute=self.fake_execute, use_multipath=True)
|
|
self.mock_object(self.connector._linuxscsi, 'get_name_from_path',
|
|
return_value="/dev/sdb")
|
|
self._fake_iqn = 'iqn.1234-56.foo.bar:01:23456789abc'
|
|
self._name = 'volume-00000001'
|
|
self._iqn = 'iqn.2010-10.org.openstack:%s' % self._name
|
|
self._location = '10.0.2.15:3260'
|
|
self._lun = 1
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_run_iscsi_session')
|
|
def test_get_iscsi_sessions_full(self, sessions_mock):
|
|
iscsiadm_result = ('tcp: [session1] ip1:port1,1 tgt1 (non-flash)\n'
|
|
'tcp: [session2] ip2:port2,-1 tgt2 (non-flash)\n'
|
|
'tcp: [session3] ip3:port3,1 tgt3\n')
|
|
sessions_mock.return_value = (iscsiadm_result, '')
|
|
res = self.connector._get_iscsi_sessions_full()
|
|
expected = [('tcp:', 'session1', 'ip1:port1', '1', 'tgt1'),
|
|
('tcp:', 'session2', 'ip2:port2', '-1', 'tgt2'),
|
|
('tcp:', 'session3', 'ip3:port3', '1', 'tgt3')]
|
|
self.assertListEqual(expected, res)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_run_iscsi_session',
|
|
return_value=(None, 'error'))
|
|
def test_get_iscsi_sessions_full_error(self, sessions_mock):
|
|
res = self.connector._get_iscsi_sessions_full()
|
|
self.assertEqual([], res)
|
|
sessions_mock.assert_called()
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions_full')
|
|
def test_get_iscsi_sessions(self, sessions_mock):
|
|
sessions_mock.return_value = [
|
|
('tcp:', 'session1', 'ip1:port1', '1', 'tgt1'),
|
|
('tcp:', 'session2', 'ip2:port2', '-1', 'tgt2'),
|
|
('tcp:', 'session3', 'ip3:port3', '1', 'tgt3')]
|
|
res = self.connector._get_iscsi_sessions()
|
|
expected = ['ip1:port1', 'ip2:port2', 'ip3:port3']
|
|
self.assertListEqual(expected, res)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions_full',
|
|
return_value=[])
|
|
def test_get_iscsi_sessions_no_sessions(self, sessions_mock):
|
|
res = self.connector._get_iscsi_sessions()
|
|
self.assertListEqual([], res)
|
|
sessions_mock.assert_called()
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_execute')
|
|
def test_get_iscsi_nodes(self, exec_mock):
|
|
iscsiadm_result = ('ip1:port1,1 tgt1\nip2:port2,-1 tgt2\n'
|
|
'ip3:port3,1 tgt3\n')
|
|
exec_mock.return_value = (iscsiadm_result, '')
|
|
res = self.connector._get_iscsi_nodes()
|
|
expected = [('ip1:port1', 'tgt1'), ('ip2:port2', 'tgt2'),
|
|
('ip3:port3', 'tgt3')]
|
|
self.assertListEqual(expected, res)
|
|
exec_mock.assert_called_once_with(
|
|
'iscsiadm', '-m', 'node', run_as_root=True,
|
|
root_helper=self.connector._root_helper, check_exit_code=False)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_execute')
|
|
def test_get_iscsi_nodes_error(self, exec_mock):
|
|
exec_mock.return_value = (None, 'error')
|
|
res = self.connector._get_iscsi_nodes()
|
|
self.assertEqual([], res)
|
|
|
|
@mock.patch('glob.glob')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions_full')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_nodes')
|
|
def test_get_connection_devices(self, nodes_mock, sessions_mock,
|
|
glob_mock):
|
|
# List sessions from other targets and non tcp sessions
|
|
sessions_mock.return_value = [
|
|
('non-tcp:', '0', 'ip1:port1', '1', 'tgt1'),
|
|
('tcp:', '1', 'ip1:port1', '1', 'tgt1'),
|
|
('tcp:', '2', 'ip2:port2', '-1', 'tgt2'),
|
|
('tcp:', '3', 'ip1:port1', '1', 'tgt4'),
|
|
('tcp:', '4', 'ip2:port2', '-1', 'tgt5')]
|
|
# List 1 node without sessions
|
|
nodes_mock.return_value = [('ip1:port1', 'tgt1'),
|
|
('ip2:port2', 'tgt2'),
|
|
('ip3:port3', 'tgt3')]
|
|
sys_cls = '/sys/class/scsi_host/host'
|
|
glob_mock.side_effect = [
|
|
[sys_cls + '1/device/session1/target6/1:2:6:4/block/sda',
|
|
sys_cls + '1/device/session1/target6/1:2:6:4/block/sda1'],
|
|
[sys_cls + '2/device/session2/target7/2:2:7:5/block/sdb',
|
|
sys_cls + '2/device/session2/target7/2:2:7:4/block/sdc'],
|
|
]
|
|
res = self.connector._get_connection_devices(self.CON_PROPS)
|
|
expected = {('ip1:port1', 'tgt1'): ({'sda'}, set()),
|
|
('ip2:port2', 'tgt2'): ({'sdb'}, {'sdc'}),
|
|
('ip3:port3', 'tgt3'): (set(), set())}
|
|
self.assertDictEqual(expected, res)
|
|
|
|
@mock.patch('glob.glob')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions_full')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_nodes')
|
|
def test_get_connection_devices_with_iqns(self, nodes_mock, sessions_mock,
|
|
glob_mock):
|
|
ips_iqns_luns = self.connector._get_all_targets(self.CON_PROPS)
|
|
|
|
# List sessions from other targets and non tcp sessions
|
|
sessions_mock.return_value = [
|
|
('non-tcp:', '0', 'ip1:port1', '1', 'tgt1'),
|
|
('tcp:', '1', 'ip1:port1', '1', 'tgt1'),
|
|
('tcp:', '2', 'ip2:port2', '-1', 'tgt2'),
|
|
('tcp:', '3', 'ip1:port1', '1', 'tgt4'),
|
|
('tcp:', '4', 'ip2:port2', '-1', 'tgt5')]
|
|
# List 1 node without sessions
|
|
nodes_mock.return_value = [('ip1:port1', 'tgt1'),
|
|
('ip2:port2', 'tgt2'),
|
|
('ip3:port3', 'tgt3')]
|
|
sys_cls = '/sys/class/scsi_host/host'
|
|
glob_mock.side_effect = [
|
|
[sys_cls + '1/device/session1/target6/1:2:6:4/block/sda',
|
|
sys_cls + '1/device/session1/target6/1:2:6:4/block/sda1'],
|
|
[sys_cls + '2/device/session2/target7/2:2:7:5/block/sdb',
|
|
sys_cls + '2/device/session2/target7/2:2:7:4/block/sdc'],
|
|
]
|
|
with mock.patch.object(iscsi.ISCSIConnector,
|
|
'_get_all_targets') as get_targets_mock:
|
|
res = self.connector._get_connection_devices(mock.sentinel.props,
|
|
ips_iqns_luns)
|
|
expected = {('ip1:port1', 'tgt1'): ({'sda'}, set()),
|
|
('ip2:port2', 'tgt2'): ({'sdb'}, {'sdc'}),
|
|
('ip3:port3', 'tgt3'): (set(), set())}
|
|
self.assertDictEqual(expected, res)
|
|
get_targets_mock.assert_not_called()
|
|
|
|
def generate_device(self, location, iqn, transport=None, lun=1):
|
|
dev_format = "ip-%s-iscsi-%s-lun-%s" % (location, iqn, lun)
|
|
if transport:
|
|
dev_format = "pci-0000:00:00.0-" + dev_format
|
|
fake_dev_path = "/dev/disk/by-path/" + dev_format
|
|
return fake_dev_path
|
|
|
|
def iscsi_connection(self, volume, location, iqn):
|
|
return {
|
|
'driver_volume_type': 'iscsi',
|
|
'data': {
|
|
'volume_id': volume['id'],
|
|
'target_portal': location,
|
|
'target_iqn': iqn,
|
|
'target_lun': 1,
|
|
}
|
|
}
|
|
|
|
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 iscsi_connection_chap(self, volume, location, iqn, auth_method,
|
|
auth_username, auth_password,
|
|
discovery_auth_method, discovery_auth_username,
|
|
discovery_auth_password):
|
|
return {
|
|
'driver_volume_type': 'iscsi',
|
|
'data': {
|
|
'auth_method': auth_method,
|
|
'auth_username': auth_username,
|
|
'auth_password': auth_password,
|
|
'discovery_auth_method': discovery_auth_method,
|
|
'discovery_auth_username': discovery_auth_username,
|
|
'discovery_auth_password': discovery_auth_password,
|
|
'target_lun': 1,
|
|
'volume_id': volume['id'],
|
|
'target_iqn': iqn,
|
|
'target_portal': location,
|
|
}
|
|
}
|
|
|
|
def _initiator_get_text(self, *arg, **kwargs):
|
|
text = ('## DO NOT EDIT OR REMOVE THIS FILE!\n'
|
|
'## If you remove this file, the iSCSI daemon '
|
|
'will not start.\n'
|
|
'## If you change the InitiatorName, existing '
|
|
'access control lists\n'
|
|
'## may reject this initiator. The InitiatorName must '
|
|
'be unique\n'
|
|
'## for each iSCSI initiator. Do NOT duplicate iSCSI '
|
|
'InitiatorNames.\n'
|
|
'InitiatorName=%s' % self._fake_iqn)
|
|
return text, None
|
|
|
|
def test_get_initiator(self):
|
|
def initiator_no_file(*args, **kwargs):
|
|
raise putils.ProcessExecutionError('No file')
|
|
|
|
self.connector._execute = initiator_no_file
|
|
initiator = self.connector.get_initiator()
|
|
self.assertIsNone(initiator)
|
|
self.connector._execute = self._initiator_get_text
|
|
initiator = self.connector.get_initiator()
|
|
self.assertEqual(initiator, self._fake_iqn)
|
|
|
|
def test_get_connector_properties(self):
|
|
with mock.patch.object(priv_rootwrap, 'execute') as mock_exec:
|
|
mock_exec.return_value = self._initiator_get_text()
|
|
multipath = True
|
|
enforce_multipath = True
|
|
props = iscsi.ISCSIConnector.get_connector_properties(
|
|
'sudo', multipath=multipath,
|
|
enforce_multipath=enforce_multipath)
|
|
|
|
expected_props = {'initiator': self._fake_iqn}
|
|
self.assertEqual(expected_props, props)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_run_iscsiadm_bare')
|
|
def test_brick_iscsi_validate_transport(self, mock_iscsiadm):
|
|
sample_output = ('# BEGIN RECORD 2.0-872\n'
|
|
'iface.iscsi_ifacename = %s.fake_suffix\n'
|
|
'iface.net_ifacename = <empty>\n'
|
|
'iface.ipaddress = <empty>\n'
|
|
'iface.hwaddress = 00:53:00:00:53:00\n'
|
|
'iface.transport_name = %s\n'
|
|
'iface.initiatorname = <empty>\n'
|
|
'# END RECORD')
|
|
for tport in self.connector.supported_transports:
|
|
mock_iscsiadm.return_value = (sample_output % (tport, tport), '')
|
|
self.assertEqual(tport + '.fake_suffix',
|
|
self.connector._validate_iface_transport(
|
|
tport + '.fake_suffix'))
|
|
|
|
mock_iscsiadm.return_value = ("", 'iscsiadm: Could not '
|
|
'read iface fake_transport (6)')
|
|
self.assertEqual('default',
|
|
self.connector._validate_iface_transport(
|
|
'fake_transport'))
|
|
|
|
def test_get_search_path(self):
|
|
search_path = self.connector.get_search_path()
|
|
expected = "/dev/disk/by-path"
|
|
self.assertEqual(expected, search_path)
|
|
|
|
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_potential_volume_paths')
|
|
def test_get_volume_paths(self, mock_potential_paths, mock_exists):
|
|
name1 = 'volume-00000001-1'
|
|
vol = {'id': 1, 'name': name1}
|
|
location = '10.0.2.15:3260'
|
|
iqn = 'iqn.2010-10.org.openstack:%s' % name1
|
|
|
|
fake_path = ("/dev/disk/by-path/ip-%(ip)s-iscsi-%(iqn)s-lun-%(lun)s" %
|
|
{'ip': '10.0.2.15', 'iqn': iqn, 'lun': 1})
|
|
fake_devices = [fake_path]
|
|
expected = fake_devices
|
|
mock_potential_paths.return_value = fake_devices
|
|
|
|
connection_properties = self.iscsi_connection(vol, [location],
|
|
[iqn])
|
|
volume_paths = self.connector.get_volume_paths(
|
|
connection_properties['data'])
|
|
self.assertEqual(expected, volume_paths)
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
|
def test_discover_mpath_device(self, mock_multipath_device,
|
|
mock_multipath_device_path):
|
|
location1 = '10.0.2.15:3260'
|
|
location2 = '[2001:db8::1]: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'
|
|
fake_raw_dev = '/dev/disk/by-path/fake-raw-lun'
|
|
vol = {'id': 1, 'name': name1}
|
|
connection_properties = self.iscsi_connection_multipath(
|
|
vol, [location1, location2], [iqn1, iqn2], [1, 2])
|
|
mock_multipath_device_path.return_value = fake_multipath_dev
|
|
mock_multipath_device.return_value = test_connector.FAKE_SCSI_WWN
|
|
(result_path, result_mpath_id) = (
|
|
self.connector_with_multipath._discover_mpath_device(
|
|
test_connector.FAKE_SCSI_WWN,
|
|
connection_properties['data'],
|
|
fake_raw_dev))
|
|
result = {'path': result_path, 'multipath_id': result_mpath_id}
|
|
expected_result = {'path': fake_multipath_dev,
|
|
'multipath_id': test_connector.FAKE_SCSI_WWN}
|
|
self.assertEqual(expected_result, result)
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device_path')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_multipath_device')
|
|
@mock.patch.object(os.path, 'realpath')
|
|
def test_discover_mpath_device_by_realpath(self, mock_realpath,
|
|
mock_multipath_device,
|
|
mock_multipath_device_path):
|
|
|
|
FAKE_SCSI_WWN = '1234567890'
|
|
location1 = '10.0.2.15:3260'
|
|
location2 = '[2001:db8::1]: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 = None
|
|
fake_raw_dev = '/dev/disk/by-path/fake-raw-lun'
|
|
vol = {'id': 1, 'name': name1}
|
|
connection_properties = self.iscsi_connection_multipath(
|
|
vol, [location1, location2], [iqn1, iqn2], [1, 2])
|
|
mock_multipath_device_path.return_value = fake_multipath_dev
|
|
mock_multipath_device.return_value = {
|
|
'device': '/dev/mapper/%s' % FAKE_SCSI_WWN}
|
|
mock_realpath.return_value = '/dev/sdvc'
|
|
(result_path, result_mpath_id) = (
|
|
self.connector_with_multipath._discover_mpath_device(
|
|
FAKE_SCSI_WWN,
|
|
connection_properties['data'],
|
|
fake_raw_dev))
|
|
mock_multipath_device.assert_called_with('/dev/sdvc')
|
|
result = {'path': result_path, 'multipath_id': result_mpath_id}
|
|
expected_result = {'path': '/dev/mapper/%s' % FAKE_SCSI_WWN,
|
|
'multipath_id': FAKE_SCSI_WWN}
|
|
self.assertEqual(expected_result, result)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_multipath_volume')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_single_volume')
|
|
def test_connect_volume_mp(self, con_single_mock, con_mp_mock, clean_mock):
|
|
self.connector.use_multipath = True
|
|
res = self.connector.connect_volume(self.CON_PROPS)
|
|
self.assertEqual(con_mp_mock.return_value, res)
|
|
con_single_mock.assert_not_called()
|
|
con_mp_mock.assert_called_once_with(self.CON_PROPS)
|
|
clean_mock.assert_not_called()
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_multipath_volume')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_single_volume')
|
|
def test_connect_volume_mp_failure(self, con_single_mock, con_mp_mock,
|
|
clean_mock):
|
|
self.connector.use_multipath = True
|
|
con_mp_mock.side_effect = exception.BrickException
|
|
self.assertRaises(exception.BrickException,
|
|
self.connector.connect_volume, self.CON_PROPS)
|
|
con_single_mock.assert_not_called()
|
|
con_mp_mock.assert_called_once_with(self.CON_PROPS)
|
|
clean_mock.assert_called_once_with(self.CON_PROPS, force=True)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_multipath_volume')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_single_volume')
|
|
def test_connect_volume_sp(self, con_single_mock, con_mp_mock, clean_mock):
|
|
self.connector.use_multipath = False
|
|
res = self.connector.connect_volume(self.CON_PROPS)
|
|
self.assertEqual(con_single_mock.return_value, res)
|
|
con_mp_mock.assert_not_called()
|
|
con_single_mock.assert_called_once_with(self.CON_PROPS)
|
|
clean_mock.assert_not_called()
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_multipath_volume')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_single_volume')
|
|
def test_connect_volume_sp_failure(self, con_single_mock, con_mp_mock,
|
|
clean_mock):
|
|
self.connector.use_multipath = False
|
|
con_single_mock.side_effect = exception.BrickException
|
|
self.assertRaises(exception.BrickException,
|
|
self.connector.connect_volume, self.CON_PROPS)
|
|
con_mp_mock.assert_not_called()
|
|
con_single_mock.assert_called_once_with(self.CON_PROPS)
|
|
clean_mock.assert_called_once_with(self.CON_PROPS, force=True)
|
|
|
|
def test_discover_iscsi_portals(self):
|
|
location = '10.0.2.15:3260'
|
|
name = 'volume-00000001'
|
|
iqn = 'iqn.2010-10.org.openstack:%s' % name
|
|
vol = {'id': 1, 'name': name}
|
|
auth_method = 'CHAP'
|
|
auth_username = 'fake_chap_username'
|
|
auth_password = 'fake_chap_password'
|
|
discovery_auth_method = 'CHAP'
|
|
discovery_auth_username = 'fake_chap_username'
|
|
discovery_auth_password = 'fake_chap_password'
|
|
connection_properties = self.iscsi_connection_chap(
|
|
vol, location, iqn, auth_method, auth_username, auth_password,
|
|
discovery_auth_method, discovery_auth_username,
|
|
discovery_auth_password)
|
|
self.connector_with_multipath = iscsi.ISCSIConnector(
|
|
None, execute=self.fake_execute, use_multipath=True)
|
|
|
|
for transport in ['default', 'iser', 'badTransport']:
|
|
interface = 'iser' if transport == 'iser' else 'default'
|
|
self.mock_object(self.connector_with_multipath, '_get_transport',
|
|
mock.Mock(return_value=interface))
|
|
|
|
self.connector_with_multipath._discover_iscsi_portals(
|
|
connection_properties['data'])
|
|
|
|
expected_cmds = [
|
|
'iscsiadm -m discoverydb -t sendtargets -I %(iface)s '
|
|
'-p %(location)s --op update '
|
|
'-n discovery.sendtargets.auth.authmethod -v %(auth_method)s '
|
|
'-n discovery.sendtargets.auth.username -v %(username)s '
|
|
'-n discovery.sendtargets.auth.password -v %(password)s' %
|
|
{'iface': interface, 'location': location,
|
|
'auth_method': discovery_auth_method,
|
|
'username': discovery_auth_username,
|
|
'password': discovery_auth_password},
|
|
'iscsiadm -m discoverydb -t sendtargets -I %(iface)s'
|
|
' -p %(location)s --discover' % {'iface': interface,
|
|
'location': location}]
|
|
self.assertEqual(expected_cmds, self.cmds)
|
|
# Reset to run with a different transport type
|
|
self.cmds = list()
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector,
|
|
'_run_iscsiadm_update_discoverydb')
|
|
@mock.patch.object(os.path, 'exists', return_value=True)
|
|
def test_iscsi_portals_with_chap_discovery(
|
|
self, exists, update_discoverydb):
|
|
location = '10.0.2.15:3260'
|
|
name = 'volume-00000001'
|
|
iqn = 'iqn.2010-10.org.openstack:%s' % name
|
|
vol = {'id': 1, 'name': name}
|
|
auth_method = 'CHAP'
|
|
auth_username = 'fake_chap_username'
|
|
auth_password = 'fake_chap_password'
|
|
discovery_auth_method = 'CHAP'
|
|
discovery_auth_username = 'fake_chap_username'
|
|
discovery_auth_password = 'fake_chap_password'
|
|
connection_properties = self.iscsi_connection_chap(
|
|
vol, location, iqn, auth_method, auth_username, auth_password,
|
|
discovery_auth_method, discovery_auth_username,
|
|
discovery_auth_password)
|
|
self.connector_with_multipath = iscsi.ISCSIConnector(
|
|
None, execute=self.fake_execute, use_multipath=True)
|
|
self.cmds = []
|
|
# The first call returns an error code = 6, mocking an empty
|
|
# discovery db. The second one mocks a successful return and the
|
|
# third one a dummy exit code, which will trigger the
|
|
# TargetPortalNotFound exception in connect_volume
|
|
update_discoverydb.side_effect = [
|
|
putils.ProcessExecutionError(None, None, 6),
|
|
("", ""),
|
|
putils.ProcessExecutionError(None, None, 9)]
|
|
|
|
self.connector_with_multipath._discover_iscsi_portals(
|
|
connection_properties['data'])
|
|
update_discoverydb.assert_called_with(connection_properties['data'])
|
|
|
|
expected_cmds = [
|
|
'iscsiadm -m discoverydb -t sendtargets -p %s -I default'
|
|
' --op new' % location,
|
|
'iscsiadm -m discoverydb -t sendtargets -I default -p %s'
|
|
' --discover' % location]
|
|
self.assertEqual(expected_cmds, self.cmds)
|
|
|
|
self.assertRaises(exception.TargetPortalNotFound,
|
|
self.connector_with_multipath.connect_volume,
|
|
connection_properties['data'])
|
|
|
|
def test_get_target_portals_from_iscsiadm_output(self):
|
|
connector = self.connector
|
|
test_output = '''10.15.84.19:3260 iqn.1992-08.com.netapp:sn.33615311
|
|
10.15.85.19:3260 iqn.1992-08.com.netapp:sn.33615311'''
|
|
res = connector._get_target_portals_from_iscsiadm_output(test_output)
|
|
ips = ['10.15.84.19:3260', '10.15.85.19:3260']
|
|
iqns = ['iqn.1992-08.com.netapp:sn.33615311',
|
|
'iqn.1992-08.com.netapp:sn.33615311']
|
|
expected = (ips, iqns)
|
|
self.assertEqual(expected, res)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
|
def test_disconnect_volume(self, cleanup_mock):
|
|
res = self.connector.disconnect_volume(mock.sentinel.con_props,
|
|
mock.sentinel.dev_info,
|
|
mock.sentinel.Force,
|
|
mock.sentinel.ignore_errors)
|
|
self.assertEqual(cleanup_mock.return_value, res)
|
|
cleanup_mock.assert_called_once_with(
|
|
mock.sentinel.con_props,
|
|
force=mock.sentinel.Force,
|
|
ignore_errors=mock.sentinel.ignore_errors)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_disconnect_connection')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_connection_devices')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'flush_multipath_device')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_connection',
|
|
return_value=None)
|
|
def test_cleanup_connection(self, remove_mock, flush_mock, con_devs_mock,
|
|
discon_mock):
|
|
# Return an ordered dicts instead of normal dict for discon_mock.assert
|
|
con_devs_mock.return_value = collections.OrderedDict((
|
|
(('ip1:port1', 'tgt1'), ({'sda'}, set())),
|
|
(('ip2:port2', 'tgt2'), ({'sdb'}, {'sdc'})),
|
|
(('ip3:port3', 'tgt3'), (set(), set()))))
|
|
|
|
with mock.patch.object(self.connector,
|
|
'use_multipath') as use_mp_mock:
|
|
self.connector._cleanup_connection(
|
|
self.CON_PROPS, ips_iqns_luns=mock.sentinel.ips_iqns_luns,
|
|
force=False, ignore_errors=False)
|
|
|
|
con_devs_mock.assert_called_once_with(self.CON_PROPS,
|
|
mock.sentinel.ips_iqns_luns)
|
|
remove_mock.assert_called_once_with({'sda', 'sdb'}, use_mp_mock,
|
|
False, mock.ANY)
|
|
discon_mock.assert_called_once_with(
|
|
self.CON_PROPS,
|
|
[('ip1:port1', 'tgt1'), ('ip3:port3', 'tgt3')],
|
|
False, mock.ANY)
|
|
flush_mock.assert_not_called()
|
|
|
|
@mock.patch('os_brick.exception.ExceptionChainer.__nonzero__',
|
|
mock.Mock(return_value=True))
|
|
@mock.patch('os_brick.exception.ExceptionChainer.__bool__',
|
|
mock.Mock(return_value=True))
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_disconnect_connection')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_connection_devices')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'flush_multipath_device')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'remove_connection',
|
|
return_value=mock.sentinel.mp_name)
|
|
def test_cleanup_connection_force_failure(self, remove_mock, flush_mock,
|
|
con_devs_mock, discon_mock):
|
|
|
|
# Return an ordered dicts instead of normal dict for discon_mock.assert
|
|
con_devs_mock.return_value = collections.OrderedDict((
|
|
(('ip1:port1', 'tgt1'), ({'sda'}, set())),
|
|
(('ip2:port2', 'tgt2'), ({'sdb'}, {'sdc'})),
|
|
(('ip3:port3', 'tgt3'), (set(), set()))))
|
|
|
|
with mock.patch.object(self.connector, 'use_multipath',
|
|
wraps=True) as use_mp_mock:
|
|
self.assertRaises(exception.ExceptionChainer,
|
|
self.connector._cleanup_connection,
|
|
self.CON_PROPS,
|
|
ips_iqns_luns=mock.sentinel.ips_iqns_luns,
|
|
force=mock.sentinel.force, ignore_errors=False)
|
|
|
|
con_devs_mock.assert_called_once_with(self.CON_PROPS,
|
|
mock.sentinel.ips_iqns_luns)
|
|
remove_mock.assert_called_once_with({'sda', 'sdb'}, use_mp_mock,
|
|
mock.sentinel.force, mock.ANY)
|
|
discon_mock.assert_called_once_with(
|
|
self.CON_PROPS,
|
|
[('ip1:port1', 'tgt1'), ('ip3:port3', 'tgt3')],
|
|
mock.sentinel.force, mock.ANY)
|
|
flush_mock.assert_called_once_with(mock.sentinel.mp_name)
|
|
|
|
@ddt.data({'do_raise': False, 'force': False},
|
|
{'do_raise': True, 'force': True},
|
|
{'do_raise': True, 'force': False})
|
|
@ddt.unpack
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_disconnect_from_iscsi_portal')
|
|
def test_disconnect_connection(self, disconnect_mock, do_raise, force):
|
|
will_raise = do_raise and not force
|
|
actual_call_args = []
|
|
|
|
# Since we reuse the copied dictionary on _disconnect_connection
|
|
# changing its values we cannot use mock's assert_has_calls
|
|
def my_disconnect(con_props):
|
|
actual_call_args.append(con_props.copy())
|
|
if do_raise:
|
|
raise exception.ExceptionChainer()
|
|
|
|
disconnect_mock.side_effect = my_disconnect
|
|
|
|
connections = (('ip1:port1', 'tgt1'), ('ip2:port2', 'tgt2'))
|
|
original_props = self.CON_PROPS.copy()
|
|
exc = exception.ExceptionChainer()
|
|
if will_raise:
|
|
self.assertRaises(exception.ExceptionChainer,
|
|
self.connector._disconnect_connection,
|
|
self.CON_PROPS, connections,
|
|
force=force, exc=exc)
|
|
else:
|
|
self.connector._disconnect_connection(self.CON_PROPS, connections,
|
|
force=force, exc=exc)
|
|
|
|
# Passed properties should not be altered by the method call
|
|
self.assertDictEqual(original_props, self.CON_PROPS)
|
|
expected = [original_props.copy(), original_props.copy()]
|
|
for i, (ip, iqn) in enumerate(connections):
|
|
expected[i].update(target_portal=ip, target_iqn=iqn)
|
|
# If we are failing and not forcing we won't make all the alls
|
|
if will_raise:
|
|
expected = expected[:1]
|
|
self.assertListEqual(expected, actual_call_args)
|
|
# No exceptions have been caught by ExceptionChainer context manager
|
|
self.assertEqual(do_raise, bool(exc))
|
|
|
|
def test_disconnect_from_iscsi_portal(self):
|
|
self.connector._disconnect_from_iscsi_portal(self.CON_PROPS)
|
|
expected_prefix = ('iscsiadm -m node -T %s -p %s ' %
|
|
(self.CON_PROPS['target_iqn'],
|
|
self.CON_PROPS['target_portal']))
|
|
expected = [
|
|
expected_prefix + '--op update -n node.startup -v manual',
|
|
expected_prefix + '--logout',
|
|
expected_prefix + '--op delete',
|
|
]
|
|
self.assertListEqual(expected, self.cmds)
|
|
|
|
def test_iscsiadm_discover_parsing(self):
|
|
# Ensure that parsing iscsiadm discover ignores cruft.
|
|
|
|
ips = ["192.168.204.82:3260,1", "192.168.204.82:3261,1"]
|
|
iqns = ["iqn.2010-10.org.openstack:volume-"
|
|
"f9b12623-6ce3-4dac-a71f-09ad4249bdd3",
|
|
"iqn.2010-10.org.openstack:volume-"
|
|
"f9b12623-6ce3-4dac-a71f-09ad4249bdd4"]
|
|
|
|
# This slight wonkiness brought to you by pep8, as the actual
|
|
# example output runs about 97 chars wide.
|
|
sample_input = """Loading iscsi modules: done
|
|
Starting iSCSI initiator service: done
|
|
Setting up iSCSI targets: unused
|
|
%s %s
|
|
%s %s
|
|
""" % (ips[0], iqns[0], ips[1], iqns[1])
|
|
out = self.connector.\
|
|
_get_target_portals_from_iscsiadm_output(sample_input)
|
|
self.assertEqual((ips, iqns), out)
|
|
|
|
def test_sanitize_log_run_iscsiadm(self):
|
|
# Tests that the parameters to the _run_iscsiadm function
|
|
# are sanitized for when passwords are logged.
|
|
def fake_debug(*args, **kwargs):
|
|
self.assertIn('node.session.auth.password', args[0])
|
|
self.assertNotIn('scrubme', args[0])
|
|
|
|
volume = {'id': 'fake_uuid'}
|
|
connection_info = self.iscsi_connection(volume,
|
|
"10.0.2.15:3260",
|
|
"fake_iqn")
|
|
|
|
iscsi_properties = connection_info['data']
|
|
with mock.patch.object(iscsi.LOG, 'debug',
|
|
side_effect=fake_debug) as debug_mock:
|
|
self.connector._iscsiadm_update(iscsi_properties,
|
|
'node.session.auth.password',
|
|
'scrubme')
|
|
|
|
# we don't care what the log message is, we just want to make sure
|
|
# our stub method is called which asserts the password is scrubbed
|
|
self.assertTrue(debug_mock.called)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, 'get_volume_paths')
|
|
def test_extend_volume_no_path(self, mock_volume_paths):
|
|
mock_volume_paths.return_value = []
|
|
volume = {'id': 'fake_uuid'}
|
|
connection_info = self.iscsi_connection(volume,
|
|
"10.0.2.15:3260",
|
|
"fake_iqn")
|
|
|
|
self.assertRaises(exception.VolumePathsNotFound,
|
|
self.connector.extend_volume,
|
|
connection_info['data'])
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume')
|
|
@mock.patch.object(iscsi.ISCSIConnector, 'get_volume_paths')
|
|
def test_extend_volume(self, mock_volume_paths, mock_scsi_extend):
|
|
fake_new_size = 1024
|
|
mock_volume_paths.return_value = ['/dev/vdx']
|
|
mock_scsi_extend.return_value = fake_new_size
|
|
volume = {'id': 'fake_uuid'}
|
|
connection_info = self.iscsi_connection(volume,
|
|
"10.0.2.15:3260",
|
|
"fake_iqn")
|
|
new_size = self.connector.extend_volume(connection_info['data'])
|
|
self.assertEqual(fake_new_size, new_size)
|
|
|
|
@mock.patch.object(iscsi.LOG, 'info')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume')
|
|
@mock.patch.object(iscsi.ISCSIConnector, 'get_volume_paths')
|
|
def test_extend_volume_mask_password(self, mock_volume_paths,
|
|
mock_scsi_extend,
|
|
mock_log_info):
|
|
fake_new_size = 1024
|
|
mock_volume_paths.return_value = ['/dev/vdx']
|
|
mock_scsi_extend.return_value = fake_new_size
|
|
volume = {'id': 'fake_uuid'}
|
|
connection_info = self.iscsi_connection_chap(
|
|
volume, "10.0.2.15:3260", "fake_iqn",
|
|
'CHAP', 'fake_user', 'fake_password',
|
|
'CHAP1', 'fake_user1', 'fake_password1')
|
|
self.connector.extend_volume(connection_info['data'])
|
|
|
|
self.assertEqual(2, mock_log_info.call_count)
|
|
self.assertIn("'auth_password': '***'",
|
|
str(mock_log_info.call_args_list[0]))
|
|
self.assertIn("'discovery_auth_password': '***'",
|
|
str(mock_log_info.call_args_list[0]))
|
|
|
|
@mock.patch.object(iscsi.LOG, 'warning')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'extend_volume')
|
|
@mock.patch.object(iscsi.ISCSIConnector, 'get_volume_paths')
|
|
def test_extend_volume_mask_password_no_paths(self, mock_volume_paths,
|
|
mock_scsi_extend,
|
|
mock_log_warning):
|
|
fake_new_size = 1024
|
|
mock_volume_paths.return_value = []
|
|
mock_scsi_extend.return_value = fake_new_size
|
|
volume = {'id': 'fake_uuid'}
|
|
connection_info = self.iscsi_connection_chap(
|
|
volume, "10.0.2.15:3260", "fake_iqn",
|
|
'CHAP', 'fake_user', 'fake_password',
|
|
'CHAP1', 'fake_user1', 'fake_password1')
|
|
|
|
self.assertRaises(exception.VolumePathsNotFound,
|
|
self.connector.extend_volume,
|
|
connection_info['data'])
|
|
|
|
self.assertEqual(1, mock_log_warning.call_count)
|
|
self.assertIn("'auth_password': '***'",
|
|
str(mock_log_warning.call_args_list[0]))
|
|
self.assertIn("'discovery_auth_password': '***'",
|
|
str(mock_log_warning.call_args_list[0]))
|
|
|
|
@mock.patch.object(os.path, 'isdir')
|
|
def test_get_all_available_volumes_path_not_dir(self, mock_isdir):
|
|
mock_isdir.return_value = False
|
|
expected = []
|
|
actual = self.connector.get_all_available_volumes()
|
|
self.assertItemsEqual(expected, actual)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_device_path')
|
|
def test_get_potential_paths_mpath(self, get_path_mock):
|
|
self.connector.use_multipath = True
|
|
res = self.connector._get_potential_volume_paths(self.CON_PROPS)
|
|
get_path_mock.assert_called_once_with(self.CON_PROPS)
|
|
self.assertEqual(get_path_mock.return_value, res)
|
|
self.assertEqual([], self.cmds)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_device_path')
|
|
def test_get_potential_paths_single_path(self, get_path_mock,
|
|
get_sessions_mock):
|
|
get_path_mock.side_effect = [['path1'], ['path2'], ['path3', 'path4']]
|
|
get_sessions_mock.return_value = [
|
|
('tcp:', 'session1', 'ip1:port1', '1', 'tgt1'),
|
|
('tcp:', 'session2', 'ip2:port2', '-1', 'tgt2'),
|
|
('tcp:', 'session3', 'ip3:port3', '1', 'tgt3')]
|
|
|
|
self.connector.use_multipath = False
|
|
res = self.connector._get_potential_volume_paths(self.CON_PROPS)
|
|
self.assertEqual({'path1', 'path2', 'path3', 'path4'}, set(res))
|
|
get_sessions_mock.assert_called_once_with()
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_discover_iscsi_portals')
|
|
def test_get_ips_iqns_luns_with_target_iqns(self, discover_mock):
|
|
res = self.connector._get_ips_iqns_luns(self.CON_PROPS)
|
|
self.assertEqual(discover_mock.return_value, res)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_discover_iscsi_portals')
|
|
def test_get_ips_iqns_luns_no_target_iqns_share_iqn(self, discover_mock):
|
|
con_props = {'volume_id': 'vol_id',
|
|
'target_portal': 'ip1:port1',
|
|
'target_iqn': 'tgt1',
|
|
'target_lun': '1'}
|
|
discover_mock.return_value = [('ip1:port1', 'tgt1', '1'),
|
|
('ip1:port1', 'tgt2', '1'),
|
|
('ip2:port2', 'tgt1', '2'),
|
|
('ip2:port2', 'tgt2', '2')]
|
|
res = self.connector._get_ips_iqns_luns(con_props)
|
|
expected = {('ip1:port1', 'tgt1', '1'),
|
|
('ip2:port2', 'tgt1', '2')}
|
|
self.assertEqual(expected, set(res))
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_discover_iscsi_portals')
|
|
def test_get_ips_iqns_luns_no_target_iqns_diff_iqn(self, discover_mock):
|
|
con_props = {'volume_id': 'vol_id',
|
|
'target_portal': 'ip1:port1',
|
|
'target_iqn': 'tgt1',
|
|
'target_lun': '1'}
|
|
discover_mock.return_value = [('ip1:port1', 'tgt1', '1'),
|
|
('ip2:port2', 'tgt2', '2')]
|
|
res = self.connector._get_ips_iqns_luns(con_props)
|
|
self.assertEqual(discover_mock.return_value, res)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions_full')
|
|
def test_connect_to_iscsi_portal_all_new(self, get_sessions_mock):
|
|
"""Connect creating node and session."""
|
|
session = 'session2'
|
|
get_sessions_mock.side_effect = [
|
|
[('tcp:', 'session1', 'ip1:port1', '1', 'tgt')],
|
|
[('tcp:', 'session1', 'ip1:port1', '1', 'tgt'),
|
|
('tcp:', session, 'ip1:port1', '-1', 'tgt1')]
|
|
]
|
|
with mock.patch.object(self.connector, '_execute') as exec_mock:
|
|
exec_mock.side_effect = [('', 'error'), ('', None),
|
|
('', None), ('', None),
|
|
('', None)]
|
|
res = self.connector._connect_to_iscsi_portal(self.CON_PROPS)
|
|
|
|
# True refers to "manual scans", since the call to update
|
|
# node.session.scan didn't fail they are set to manual
|
|
self.assertEqual((session, True), res)
|
|
prefix = 'iscsiadm -m node -T tgt1 -p ip1:port1'
|
|
expected_cmds = [
|
|
prefix,
|
|
prefix + ' --interface default --op new',
|
|
prefix + ' --op update -n node.session.scan -v manual',
|
|
prefix + ' --login',
|
|
prefix + ' --op update -n node.startup -v automatic'
|
|
]
|
|
actual_cmds = [' '.join(args[0]) for args in exec_mock.call_args_list]
|
|
self.assertListEqual(expected_cmds, actual_cmds)
|
|
self.assertEqual(2, get_sessions_mock.call_count)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions_full')
|
|
def test_connect_to_iscsi_portal_all_exists_chap(self, get_sessions_mock):
|
|
"""Node and session already exists and we use chap authentication."""
|
|
session = 'session2'
|
|
get_sessions_mock.return_value = [('tcp:', session, 'ip1:port1',
|
|
'-1', 'tgt1')]
|
|
con_props = self.CON_PROPS.copy()
|
|
con_props.update(auth_method='CHAP', auth_username='user',
|
|
auth_password='pwd')
|
|
res = self.connector._connect_to_iscsi_portal(con_props)
|
|
# False refers to "manual scans", so we have automatic iscsi scans
|
|
self.assertEqual((session, False), res)
|
|
prefix = 'iscsiadm -m node -T tgt1 -p ip1:port1'
|
|
expected_cmds = [
|
|
prefix,
|
|
prefix + ' --op update -n node.session.auth.authmethod -v CHAP',
|
|
prefix + ' --op update -n node.session.auth.username -v user',
|
|
prefix + ' --op update -n node.session.auth.password -v pwd',
|
|
]
|
|
self.assertListEqual(expected_cmds, self.cmds)
|
|
get_sessions_mock.assert_called_once_with()
|
|
|
|
@ddt.data('auto', 'manual')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions_full')
|
|
def test_connect_to_iscsi_portal_manual_scan_feature(self, manual_scan,
|
|
get_sessions_mock):
|
|
"""Node and session already exists and iscsi supports manual scans."""
|
|
session = 'session2'
|
|
get_sessions_mock.return_value = [('tcp:', session, 'ip1:port1',
|
|
'-1', 'tgt1')]
|
|
con_props = self.CON_PROPS.copy()
|
|
node_props = ('node.startup = automatic\nnode.session.scan = ' +
|
|
manual_scan)
|
|
with mock.patch.object(self.connector, '_execute') as exec_mock:
|
|
exec_mock.side_effect = [(node_props, None)]
|
|
res = self.connector._connect_to_iscsi_portal(con_props)
|
|
# False refers to "manual scans", so we have automatic iscsi scans
|
|
self.assertEqual((session, manual_scan == 'manual'), res)
|
|
|
|
actual_cmds = [' '.join(args[0]) for args in exec_mock.call_args_list]
|
|
self.assertListEqual(['iscsiadm -m node -T tgt1 -p ip1:port1'],
|
|
actual_cmds)
|
|
get_sessions_mock.assert_called_once_with()
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_get_iscsi_sessions_full')
|
|
def test_connect_to_iscsi_portal_fail_login(self, get_sessions_mock):
|
|
get_sessions_mock.return_value = []
|
|
with mock.patch.object(self.connector, '_execute') as exec_mock:
|
|
exec_mock.side_effect = [('', None), putils.ProcessExecutionError]
|
|
res = self.connector._connect_to_iscsi_portal(self.CON_PROPS)
|
|
self.assertEqual((None, None), res)
|
|
expected_cmds = ['iscsiadm -m node -T tgt1 -p ip1:port1',
|
|
'iscsiadm -m node -T tgt1 -p ip1:port1 --login']
|
|
actual_cmds = [' '.join(args[0]) for args in exec_mock.call_args_list]
|
|
self.assertListEqual(expected_cmds, actual_cmds)
|
|
get_sessions_mock.assert_called_once_with()
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
|
side_effect=(None, 'tgt2'))
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
|
@mock.patch('time.sleep')
|
|
def test_connect_single_volume(self, sleep_mock, cleanup_mock,
|
|
connect_mock, get_wwn_mock):
|
|
def my_connect(rescans, props, data):
|
|
if props['target_iqn'] == 'tgt2':
|
|
# Succeed on second call
|
|
data['found_devices'].append('sdz')
|
|
|
|
connect_mock.side_effect = my_connect
|
|
|
|
res = self.connector._connect_single_volume(self.CON_PROPS)
|
|
|
|
expected = {'type': 'block', 'scsi_wwn': 'tgt2', 'path': '/dev/sdz'}
|
|
self.assertEqual(expected, res)
|
|
get_wwn_mock.assert_has_calls([mock.call(['sdz']), mock.call(['sdz'])])
|
|
sleep_mock.assert_called_once_with(1)
|
|
cleanup_mock.assert_called_once_with(
|
|
{'target_lun': 4, 'volume_id': 'vol_id',
|
|
'target_portal': 'ip1:port1', 'target_iqn': 'tgt1'},
|
|
(('ip1:port1', 'tgt1', 4),),
|
|
force=True, ignore_errors=True)
|
|
|
|
@staticmethod
|
|
def _get_connect_vol_data():
|
|
return {'stop_connecting': False, 'num_logins': 0, 'failed_logins': 0,
|
|
'stopped_threads': 0, 'found_devices': [],
|
|
'just_added_devices': []}
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
|
side_effect=(None, 'tgt2'))
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_cleanup_connection')
|
|
@mock.patch('time.sleep')
|
|
def test_connect_single_volume_not_found(self, sleep_mock, cleanup_mock,
|
|
connect_mock, get_wwn_mock):
|
|
|
|
self.assertRaises(exception.VolumeDeviceNotFound,
|
|
self.connector._connect_single_volume,
|
|
self.CON_PROPS)
|
|
|
|
get_wwn_mock.assert_not_called()
|
|
|
|
# Called twice by the retry mechanism
|
|
self.assertEqual(2, sleep_mock.call_count)
|
|
|
|
props = list(self.connector._get_all_targets(self.CON_PROPS))
|
|
calls_per_try = [
|
|
mock.call({'target_portal': prop[0], 'target_iqn': prop[1],
|
|
'target_lun': prop[2], 'volume_id': 'vol_id'},
|
|
(prop,), force=True, ignore_errors=True)
|
|
for prop in props
|
|
]
|
|
cleanup_mock.assert_has_calls(calls_per_try * 3)
|
|
|
|
data = self._get_connect_vol_data()
|
|
calls_per_try = [mock.call(self.connector.device_scan_attempts,
|
|
{'target_portal': prop[0],
|
|
'target_iqn': prop[1],
|
|
'target_lun': prop[2],
|
|
'volume_id': 'vol_id'},
|
|
data)
|
|
for prop in props]
|
|
connect_mock.assert_has_calls(calls_per_try * 3)
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
|
side_effect=[None, 'dm-0'])
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
|
return_value='wwn')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_path')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_wwid')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
|
@mock.patch('time.sleep')
|
|
def test_connect_multipath_volume_all_succeed(self, sleep_mock,
|
|
connect_mock, add_wwid_mock,
|
|
add_path_mock, get_wwn_mock,
|
|
find_dm_mock):
|
|
def my_connect(rescans, props, data):
|
|
devs = {'tgt1': 'sda', 'tgt2': 'sdb', 'tgt3': 'sdc', 'tgt4': 'sdd'}
|
|
data['stopped_threads'] += 1
|
|
data['num_logins'] += 1
|
|
dev = devs[props['target_iqn']]
|
|
data['found_devices'].append(dev)
|
|
data['just_added_devices'].append(dev)
|
|
|
|
connect_mock.side_effect = my_connect
|
|
|
|
res = self.connector._connect_multipath_volume(self.CON_PROPS)
|
|
|
|
expected = {'type': 'block', 'scsi_wwn': 'wwn', 'multipath_id': 'dm-0',
|
|
'path': '/dev/dm-0'}
|
|
self.assertEqual(expected, res)
|
|
|
|
self.assertEqual(1, get_wwn_mock.call_count)
|
|
result = list(get_wwn_mock.call_args[0][0])
|
|
result.sort()
|
|
self.assertEqual(['sda', 'sdb', 'sdc', 'sdd'], result)
|
|
add_wwid_mock.assert_called_once_with('wwn')
|
|
self.assertNotEqual(0, add_path_mock.call_count)
|
|
self.assertGreaterEqual(find_dm_mock.call_count, 2)
|
|
self.assertEqual(4, connect_mock.call_count)
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
|
side_effect=[None, 'dm-0'])
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
|
return_value='wwn')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_path')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_wwid')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
|
@mock.patch('time.sleep')
|
|
def test_connect_multipath_volume_all_fail(self, sleep_mock, connect_mock,
|
|
add_wwid_mock, add_path_mock,
|
|
get_wwn_mock, find_dm_mock):
|
|
def my_connect(rescans, props, data):
|
|
data['stopped_threads'] += 1
|
|
data['failed_logins'] += 1
|
|
|
|
connect_mock.side_effect = my_connect
|
|
|
|
self.assertRaises(exception.VolumeDeviceNotFound,
|
|
self.connector._connect_multipath_volume,
|
|
self.CON_PROPS)
|
|
|
|
get_wwn_mock.assert_not_called()
|
|
add_wwid_mock.assert_not_called()
|
|
add_path_mock.assert_not_called()
|
|
find_dm_mock.assert_not_called()
|
|
self.assertEqual(4 * 3, connect_mock.call_count)
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
|
side_effect=[None, 'dm-0'])
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
|
return_value='wwn')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_path')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_wwid')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
|
@mock.patch('time.sleep')
|
|
def test_connect_multipath_volume_some_fail_mp_found(self, sleep_mock,
|
|
connect_mock,
|
|
add_wwid_mock,
|
|
add_path_mock,
|
|
get_wwn_mock,
|
|
find_dm_mock):
|
|
def my_connect(rescans, props, data):
|
|
devs = {'tgt1': '', 'tgt2': 'sdb', 'tgt3': '', 'tgt4': 'sdd'}
|
|
data['stopped_threads'] += 1
|
|
dev = devs[props['target_iqn']]
|
|
if dev:
|
|
data['num_logins'] += 1
|
|
data['found_devices'].append(dev)
|
|
data['just_added_devices'].append(dev)
|
|
else:
|
|
data['failed_logins'] += 1
|
|
|
|
connect_mock.side_effect = my_connect
|
|
|
|
res = self.connector._connect_multipath_volume(self.CON_PROPS)
|
|
|
|
expected = {'type': 'block', 'scsi_wwn': 'wwn', 'multipath_id': 'dm-0',
|
|
'path': '/dev/dm-0'}
|
|
self.assertEqual(expected, res)
|
|
self.assertEqual(1, get_wwn_mock.call_count)
|
|
result = list(get_wwn_mock.call_args[0][0])
|
|
result.sort()
|
|
self.assertEqual(['sdb', 'sdd'], result)
|
|
add_wwid_mock.assert_called_once_with('wwn')
|
|
self.assertNotEqual(0, add_path_mock.call_count)
|
|
self.assertGreaterEqual(find_dm_mock.call_count, 2)
|
|
self.assertEqual(4, connect_mock.call_count)
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
|
return_value=None)
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
|
return_value='wwn')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_path')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_wwid')
|
|
@mock.patch.object(iscsi.time, 'time', side_effect=(0, 0, 11, 0))
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
|
@mock.patch('time.sleep')
|
|
def test_connect_multipath_volume_some_fail_mp_not_found(self, sleep_mock,
|
|
connect_mock,
|
|
time_mock,
|
|
add_wwid_mock,
|
|
add_path_mock,
|
|
get_wwn_mock,
|
|
find_dm_mock):
|
|
def my_connect(rescans, props, data):
|
|
devs = {'tgt1': '', 'tgt2': 'sdb', 'tgt3': '', 'tgt4': 'sdd'}
|
|
data['stopped_threads'] += 1
|
|
dev = devs[props['target_iqn']]
|
|
if dev:
|
|
data['num_logins'] += 1
|
|
data['found_devices'].append(dev)
|
|
data['just_added_devices'].append(dev)
|
|
else:
|
|
data['failed_logins'] += 1
|
|
|
|
connect_mock.side_effect = my_connect
|
|
|
|
res = self.connector._connect_multipath_volume(self.CON_PROPS)
|
|
|
|
expected = [{'type': 'block', 'scsi_wwn': 'wwn', 'path': '/dev/sdb'},
|
|
{'type': 'block', 'scsi_wwn': 'wwn', 'path': '/dev/sdd'}]
|
|
# It can only be one of the 2
|
|
self.assertIn(res, expected)
|
|
self.assertEqual(1, get_wwn_mock.call_count)
|
|
result = list(get_wwn_mock.call_args[0][0])
|
|
result.sort()
|
|
self.assertEqual(['sdb', 'sdd'], result)
|
|
add_wwid_mock.assert_called_once_with('wwn')
|
|
self.assertNotEqual(0, add_path_mock.call_count)
|
|
self.assertGreaterEqual(find_dm_mock.call_count, 4)
|
|
self.assertEqual(4, connect_mock.call_count)
|
|
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'find_sysfs_multipath_dm',
|
|
return_value=None)
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'get_sysfs_wwn',
|
|
return_value='wwn')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_path')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'multipath_add_wwid')
|
|
@mock.patch.object(iscsi.time, 'time', side_effect=(0, 0, 11, 0))
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_vol')
|
|
@mock.patch('time.sleep', mock.Mock())
|
|
def test_connect_multipath_volume_all_loging_not_found(self,
|
|
connect_mock,
|
|
time_mock,
|
|
add_wwid_mock,
|
|
add_path_mock,
|
|
get_wwn_mock,
|
|
find_dm_mock):
|
|
def my_connect(rescans, props, data):
|
|
data['stopped_threads'] += 1
|
|
data['num_logins'] += 1
|
|
|
|
connect_mock.side_effect = my_connect
|
|
|
|
self.assertRaises(exception.VolumeDeviceNotFound,
|
|
self.connector._connect_multipath_volume,
|
|
self.CON_PROPS)
|
|
|
|
get_wwn_mock.assert_not_called()
|
|
add_wwid_mock.assert_not_called()
|
|
add_path_mock.assert_not_called()
|
|
find_dm_mock.assert_not_called()
|
|
self.assertEqual(12, connect_mock.call_count)
|
|
|
|
@mock.patch('time.sleep', mock.Mock())
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'scan_iscsi')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'device_name_by_hctl',
|
|
return_value='sda')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal')
|
|
def test_connect_vol(self, connect_mock, dev_name_mock, scan_mock):
|
|
lscsi = self.connector._linuxscsi
|
|
data = self._get_connect_vol_data()
|
|
hctl = [mock.sentinel.host, mock.sentinel.channel,
|
|
mock.sentinel.target, mock.sentinel.lun]
|
|
|
|
connect_mock.return_value = (mock.sentinel.session, False)
|
|
|
|
with mock.patch.object(lscsi, 'get_hctl',
|
|
side_effect=(None, hctl)) as hctl_mock:
|
|
self.connector._connect_vol(3, self.CON_PROPS, data)
|
|
|
|
expected = self._get_connect_vol_data()
|
|
expected.update(num_logins=1, stopped_threads=1,
|
|
found_devices=['sda'], just_added_devices=['sda'])
|
|
self.assertDictEqual(expected, data)
|
|
|
|
connect_mock.assert_called_once_with(self.CON_PROPS)
|
|
hctl_mock.assert_has_calls([mock.call(mock.sentinel.session,
|
|
self.CON_PROPS['target_lun']),
|
|
mock.call(mock.sentinel.session,
|
|
self.CON_PROPS['target_lun'])])
|
|
|
|
scan_mock.assert_called_once_with(*hctl)
|
|
dev_name_mock.assert_called_once_with(mock.sentinel.session, hctl)
|
|
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal',
|
|
return_value=(None, False))
|
|
def test_connect_vol_no_session(self, connect_mock):
|
|
data = self._get_connect_vol_data()
|
|
|
|
self.connector._connect_vol(3, self.CON_PROPS, data)
|
|
|
|
expected = self._get_connect_vol_data()
|
|
expected.update(failed_logins=1, stopped_threads=1)
|
|
self.assertDictEqual(expected, data)
|
|
|
|
@mock.patch('time.sleep', mock.Mock())
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'scan_iscsi')
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'device_name_by_hctl',
|
|
return_value=None)
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal')
|
|
def test_connect_vol_not_found(self, connect_mock, dev_name_mock,
|
|
scan_mock):
|
|
lscsi = self.connector._linuxscsi
|
|
data = self._get_connect_vol_data()
|
|
hctl = [mock.sentinel.host, mock.sentinel.channel,
|
|
mock.sentinel.target, mock.sentinel.lun]
|
|
|
|
# True because we are simulating we have manual scans
|
|
connect_mock.return_value = (mock.sentinel.session, True)
|
|
|
|
with mock.patch.object(lscsi, 'get_hctl',
|
|
side_effect=(hctl,)) as hctl_mock:
|
|
self.connector._connect_vol(3, self.CON_PROPS, data)
|
|
|
|
expected = self._get_connect_vol_data()
|
|
expected.update(num_logins=1, stopped_threads=1)
|
|
self.assertDictEqual(expected, data)
|
|
|
|
hctl_mock.assert_called_once_with(mock.sentinel.session,
|
|
self.CON_PROPS['target_lun'])
|
|
# We have 3 scans because on manual mode we also scan on connect
|
|
scan_mock.assert_has_calls([mock.call(*hctl)] * 3)
|
|
dev_name_mock.assert_has_calls(
|
|
[mock.call(mock.sentinel.session, hctl),
|
|
mock.call(mock.sentinel.session, hctl)])
|
|
|
|
@mock.patch('time.sleep', mock.Mock())
|
|
@mock.patch.object(linuxscsi.LinuxSCSI, 'scan_iscsi')
|
|
@mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal')
|
|
def test_connect_vol_stop_connecting(self, connect_mock, scan_mock):
|
|
data = self._get_connect_vol_data()
|
|
|
|
def device_name_by_hctl(session, hctl):
|
|
data['stop_connecting'] = True
|
|
return None
|
|
|
|
lscsi = self.connector._linuxscsi
|
|
hctl = [mock.sentinel.host, mock.sentinel.channel,
|
|
mock.sentinel.target, mock.sentinel.lun]
|
|
|
|
connect_mock.return_value = (mock.sentinel.session, False)
|
|
|
|
with mock.patch.object(lscsi, 'get_hctl',
|
|
return_value=hctl) as hctl_mock, \
|
|
mock.patch.object(
|
|
lscsi, 'device_name_by_hctl',
|
|
side_effect=device_name_by_hctl) as dev_name_mock:
|
|
|
|
self.connector._connect_vol(3, self.CON_PROPS, data)
|
|
|
|
expected = self._get_connect_vol_data()
|
|
expected.update(num_logins=1, stopped_threads=1, stop_connecting=True)
|
|
self.assertDictEqual(expected, data)
|
|
|
|
hctl_mock.assert_called_once_with(mock.sentinel.session,
|
|
self.CON_PROPS['target_lun'])
|
|
scan_mock.assert_not_called()
|
|
dev_name_mock.assert_called_once_with(mock.sentinel.session, hctl)
|