diff --git a/os_brick/initiator/connectors/iscsi.py b/os_brick/initiator/connectors/iscsi.py index 873920918..9716658f6 100644 --- a/os_brick/initiator/connectors/iscsi.py +++ b/os_brick/initiator/connectors/iscsi.py @@ -32,6 +32,7 @@ from os_brick.i18n import _ from os_brick import initiator from os_brick.initiator.connectors import base from os_brick.initiator.connectors import base_iscsi +from os_brick.initiator import utils as initiator_utils from os_brick import utils synchronized = lockutils.synchronized_with_prefix('os-brick-') @@ -1030,6 +1031,11 @@ class ISCSIConnector(base.BaseLinuxConnector, base_iscsi.BaseISCSIConnector): 'node.session.scan', 'manual', check_exit_code=False) manual_scan = not res[1] + # Update global indicator of manual scan support used for + # shared_targets locking so we support upgrading open iscsi to a + # version supporting the manual scan feature without restarting Nova + # or Cinder. + initiator_utils.ISCSI_SUPPORTS_MANUAL_SCAN = manual_scan if connection_properties.get('auth_method'): self._iscsiadm_update(connection_properties, diff --git a/os_brick/initiator/utils.py b/os_brick/initiator/utils.py new file mode 100644 index 000000000..ae4b0af88 --- /dev/null +++ b/os_brick/initiator/utils.py @@ -0,0 +1,46 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# 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 contextlib +import os + +from oslo_concurrency import lockutils +from oslo_concurrency import processutils as putils + + +def check_manual_scan(): + if os.name == 'nt': + return False + + try: + putils.execute('grep', '-F', 'node.session.scan', '/sbin/iscsiadm') + except putils.ProcessExecutionError: + return False + return True + + +ISCSI_SUPPORTS_MANUAL_SCAN = check_manual_scan() + + +@contextlib.contextmanager +def guard_connection(device): + """Context Manager handling locks for attach/detach operations.""" + if ISCSI_SUPPORTS_MANUAL_SCAN or not device.get('shared_targets'): + yield + else: + # Cinder passes an OVO, but Nova passes a dictionary, so we use dict + # key access that works with both. + with lockutils.lock(device['service_uuid'], 'os-brick-'): + yield diff --git a/os_brick/tests/initiator/connectors/test_iscsi.py b/os_brick/tests/initiator/connectors/test_iscsi.py index 8f1c9ff07..83af738c4 100644 --- a/os_brick/tests/initiator/connectors/test_iscsi.py +++ b/os_brick/tests/initiator/connectors/test_iscsi.py @@ -21,6 +21,7 @@ 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.initiator import utils from os_brick.privileged import rootwrap as priv_rootwrap from os_brick.tests.initiator import test_connector @@ -987,6 +988,7 @@ Setting up iSCSI targets: unused [('tcp:', 'session1', 'ip1:port1', '1', 'tgt'), ('tcp:', session, 'ip1:port1', '-1', 'tgt1')] ] + utils.ISCSI_SUPPORTS_MANUAL_SCAN = None with mock.patch.object(self.connector, '_execute') as exec_mock: exec_mock.side_effect = [('', 'error'), ('', None), ('', None), ('', None), @@ -996,6 +998,7 @@ Setting up iSCSI targets: unused # 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) + self.assertTrue(utils.ISCSI_SUPPORTS_MANUAL_SCAN) prefix = 'iscsiadm -m node -T tgt1 -p ip1:port1' expected_cmds = [ prefix, @@ -1018,6 +1021,7 @@ Setting up iSCSI targets: unused [('tcp:', 'session1', 'Ip1:port1', '1', 'tgt'), ('tcp:', session, 'IP1:port1', '-1', 'tgt1')] ] + utils.ISCSI_SUPPORTS_MANUAL_SCAN = None with mock.patch.object(self.connector, '_execute') as exec_mock: exec_mock.side_effect = [('', 'error'), ('', None), ('', None), ('', None), @@ -1027,6 +1031,7 @@ Setting up iSCSI targets: unused # 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) + self.assertTrue(utils.ISCSI_SUPPORTS_MANUAL_SCAN) prefix = 'iscsiadm -m node -T tgt1 -p ip1:port1' expected_cmds = [ prefix, @@ -1048,9 +1053,11 @@ Setting up iSCSI targets: unused con_props = self.CON_PROPS.copy() con_props.update(auth_method='CHAP', auth_username='user', auth_password='pwd') + utils.ISCSI_SUPPORTS_MANUAL_SCAN = None res = self.connector._connect_to_iscsi_portal(con_props) # False refers to "manual scans", so we have manual iscsi scans self.assertEqual((session, True), res) + self.assertTrue(utils.ISCSI_SUPPORTS_MANUAL_SCAN) prefix = 'iscsiadm -m node -T tgt1 -p ip1:port1' expected_cmds = [ prefix, diff --git a/os_brick/tests/initiator/test_utils.py b/os_brick/tests/initiator/test_utils.py new file mode 100644 index 000000000..57f701439 --- /dev/null +++ b/os_brick/tests/initiator/test_utils.py @@ -0,0 +1,62 @@ +# Copyright 2018 Red Hat, Inc. +# All Rights Reserved. +# +# 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 mock + +from os_brick.initiator import utils +from os_brick.tests import base + + +class InitiatorUtilsTestCase(base.TestCase): + + @mock.patch('os.name', 'nt') + def test_check_manual_scan_windows(self): + self.assertFalse(utils.check_manual_scan()) + + @mock.patch('os.name', 'posix') + @mock.patch('oslo_concurrency.processutils.execute') + def test_check_manual_scan_supported(self, mock_exec): + self.assertTrue(utils.check_manual_scan()) + mock_exec.assert_called_once_with('grep', '-F', 'node.session.scan', + '/sbin/iscsiadm') + + @mock.patch('os.name', 'posix') + @mock.patch('oslo_concurrency.processutils.execute', + side_effect=utils.putils.ProcessExecutionError) + def test_check_manual_scan_not_supported(self, mock_exec): + self.assertFalse(utils.check_manual_scan()) + mock_exec.assert_called_once_with('grep', '-F', 'node.session.scan', + '/sbin/iscsiadm') + + @mock.patch('oslo_concurrency.lockutils.lock') + def test_guard_connection_manual_scan_support(self, mock_lock): + utils.ISCSI_SUPPORTS_MANUAL_SCAN = True + # We confirm that shared_targets is ignored + with utils.guard_connection({'shared_targets': True}): + mock_lock.assert_not_called() + + @mock.patch('oslo_concurrency.lockutils.lock') + def test_guard_connection_manual_scan_unsupported_not_shared(self, + mock_lock): + utils.ISCSI_SUPPORTS_MANUAL_SCAN = False + with utils.guard_connection({'shared_targets': False}): + mock_lock.assert_not_called() + + @mock.patch('oslo_concurrency.lockutils.lock') + def test_guard_connection_manual_scan_unsupported_hared(self, mock_lock): + utils.ISCSI_SUPPORTS_MANUAL_SCAN = False + with utils.guard_connection({'service_uuid': mock.sentinel.uuid, + 'shared_targets': True}): + mock_lock.assert_called_once_with(mock.sentinel.uuid, 'os-brick-')