From f67d46c5383b3c454f365b653a36d3cd043a7814 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Thu, 6 Apr 2017 17:55:15 +0200 Subject: [PATCH] Add open-iscsi manual scan support 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 --- os_brick/initiator/connectors/iscsi.py | 20 +++++-- .../tests/initiator/connectors/test_iscsi.py | 57 ++++++++++++++----- ..._manual_scan_support-d64a1c3c8e1986b4.yaml | 11 ++++ 3 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/iscsi_manual_scan_support-d64a1c3c8e1986b4.yaml 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.