diff --git a/os_brick/initiator/connectors/iscsi.py b/os_brick/initiator/connectors/iscsi.py index 6c94796d8..04ecef05a 100644 --- a/os_brick/initiator/connectors/iscsi.py +++ b/os_brick/initiator/connectors/iscsi.py @@ -481,7 +481,7 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): """ device = hctl = None portal = props['target_portal'] - session = self._connect_to_iscsi_portal(props) + session, manual_scan = self._connect_to_iscsi_portal(props) do_scans = rescans > 0 retry = 1 if session: @@ -493,8 +493,9 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): hctl = self._linuxscsi.get_hctl(session, props['target_lun']) # Scan is sent on connect by iscsid, so skip first rescan + # but on manual scan mode we have to do it ourselves. if hctl: - if retry > 1: + if retry > 1 or manual_scan: self._linuxscsi.scan_iscsi(*hctl) device = self._linuxscsi.device_name_by_hctl(session, @@ -846,12 +847,19 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): # volume is using the same target. # iscsiadm returns 21 for "No records found" after version 2.0-871 LOG.info("Trying to connect to iSCSI portal %s", portal) - err = self._run_iscsiadm(connection_properties, (), - check_exit_code=(0, 21, 255))[1] + out, err = self._run_iscsiadm(connection_properties, (), + check_exit_code=(0, 21, 255)) if err: self._run_iscsiadm(connection_properties, ('--interface', self._get_transport(), '--op', 'new')) + # Try to set the scan mode to manual + res = self._iscsiadm_update(connection_properties, + 'node.session.scan', 'manual', + check_exit_code=False) + manual_scan = not res[1] + else: + manual_scan = 'node.session.scan = manual' in out if connection_properties.get('auth_method'): self._iscsiadm_update(connection_properties, @@ -872,7 +880,7 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): for s in sessions: # Found our session, return session_id if 'tcp:' == s[0] and portal == s[2] and s[4] == target_iqn: - return s[1] + return s[1], manual_scan try: # exit_code=15 means the session already exists, so it should @@ -884,7 +892,7 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): '%(portal)s (exit code %(err)s).', {'iqn': target_iqn, 'portal': portal, 'err': err.exit_code}) - return None + return None, None self._iscsiadm_update(connection_properties, "node.startup", diff --git a/os_brick/tests/initiator/connectors/test_iscsi.py b/os_brick/tests/initiator/connectors/test_iscsi.py index 1c3ded89b..61f1caa50 100644 --- a/os_brick/tests/initiator/connectors/test_iscsi.py +++ b/os_brick/tests/initiator/connectors/test_iscsi.py @@ -831,14 +831,19 @@ Setting up iSCSI targets: unused ('tcp:', session, 'ip1:port1', '-1', 'tgt1')] ] with mock.patch.object(self.connector, '_execute') as exec_mock: - exec_mock.side_effect = [('', 'error'), ('', None), ('', None), + exec_mock.side_effect = [('', 'error'), ('', None), + ('', None), ('', None), ('', None)] res = self.connector._connect_to_iscsi_portal(self.CON_PROPS) - self.assertEqual(session, res) + + # 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' ] @@ -856,7 +861,8 @@ Setting up iSCSI targets: unused con_props.update(auth_method='CHAP', auth_username='user', auth_password='pwd') res = self.connector._connect_to_iscsi_portal(con_props) - self.assertEqual(session, res) + # 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, @@ -867,13 +873,35 @@ Setting up iSCSI targets: unused 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.assertIsNone(res) + 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] @@ -1140,7 +1168,7 @@ Setting up iSCSI targets: unused hctl = [mock.sentinel.host, mock.sentinel.channel, mock.sentinel.target, mock.sentinel.lun] - connect_mock.return_value = mock.sentinel.session + connect_mock.return_value = (mock.sentinel.session, False) with mock.patch.object(lscsi, 'get_hctl', side_effect=(None, hctl)) as hctl_mock: @@ -1161,7 +1189,7 @@ Setting up iSCSI targets: unused dev_name_mock.assert_called_once_with(mock.sentinel.session, hctl) @mock.patch.object(iscsi.ISCSIConnector, '_connect_to_iscsi_portal', - return_value=None) + return_value=(None, False)) def test_connect_vol_no_session(self, connect_mock): data = self._get_connect_vol_data() @@ -1183,22 +1211,21 @@ Setting up iSCSI targets: unused hctl = [mock.sentinel.host, mock.sentinel.channel, mock.sentinel.target, mock.sentinel.lun] - connect_mock.return_value = mock.sentinel.session + # 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=(None, hctl)) as hctl_mock: + 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_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_has_calls([mock.call(*hctl), mock.call(*hctl)]) + 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)]) @@ -1217,7 +1244,7 @@ Setting up iSCSI targets: unused hctl = [mock.sentinel.host, mock.sentinel.channel, mock.sentinel.target, mock.sentinel.lun] - connect_mock.return_value = mock.sentinel.session + connect_mock.return_value = (mock.sentinel.session, False) with mock.patch.object(lscsi, 'get_hctl', return_value=hctl) as hctl_mock, \ diff --git a/releasenotes/notes/iscsi_manual_scan_support-d64a1c3c8e1986b4.yaml b/releasenotes/notes/iscsi_manual_scan_support-d64a1c3c8e1986b4.yaml new file mode 100644 index 000000000..40020c7f8 --- /dev/null +++ b/releasenotes/notes/iscsi_manual_scan_support-d64a1c3c8e1986b4.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Support for setting the scan mode on the Open-iSCSI initiator. If + installed iscsiadm supports this feature OS-Brick will set all it's new + sessions to manual scan. +fixes: + - | + On systems with scan mode support on open-iSCSI we'll no longer see + unwanted devices polluting our system due to the automatic initiator scan + or to AEN/AER messages from the backend.