diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_mirror_fc.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_mirror_fc.py index a13ef517f53..6b4fabeafc6 100644 --- a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_mirror_fc.py +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_mirror_fc.py @@ -750,7 +750,11 @@ class HBSDMIRRORFCDriverTest(test.TestCase): group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_host_mode_options', [], group=conf.SHARED_CONF_GROUP) - + self.override_config('hitachi_rest_use_object_caching', False, + group=conf.SHARED_CONF_GROUP) + self.override_config('hitachi_rest_max_request_workers', + hbsd_rest_api._MAX_REQUEST_WORKERS, + group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_zoning_request', False, group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_extend_snapshot_volumes', False, diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_replication_fc.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_replication_fc.py index 67c3865c21d..85bb3792fba 100644 --- a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_replication_fc.py +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_replication_fc.py @@ -832,7 +832,11 @@ class HBSDREPLICATIONFCDriverTest(test.TestCase): group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_host_mode_options', [], group=conf.SHARED_CONF_GROUP) - + self.override_config('hitachi_rest_use_object_caching', False, + group=conf.SHARED_CONF_GROUP) + self.override_config('hitachi_rest_max_request_workers', + hbsd_rest_api._MAX_REQUEST_WORKERS, + group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_zoning_request', False, group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_extend_snapshot_volumes', False, diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_fc.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_fc.py index f54cb42c676..b79b4a24925 100644 --- a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_fc.py +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_fc.py @@ -771,7 +771,11 @@ class HBSDRESTFCDriverTest(test.TestCase): group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_host_mode_options', [], group=conf.SHARED_CONF_GROUP) - + self.override_config('hitachi_rest_use_object_caching', False, + group=conf.SHARED_CONF_GROUP) + self.override_config('hitachi_rest_max_request_workers', + hbsd_rest_api._MAX_REQUEST_WORKERS, + group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_zoning_request', False, group=conf.SHARED_CONF_GROUP) @@ -913,6 +917,9 @@ class HBSDRESTFCDriverTest(test.TestCase): self.driver.create_export_snapshot(None, None, None) self.driver.remove_export_snapshot(None, None) # stop the Loopingcall within the do_setup treatment + # We also added an initial delay of the actual session value + # since the initialization with threads is slower than + # the initial run of the looping call. self.driver.common.client.keep_session_loop.stop() def tearDown(self): @@ -939,6 +946,9 @@ class HBSDRESTFCDriverTest(test.TestCase): self.assertEqual(1, brick_get_connector_properties.call_count) self.assertEqual(4, request.call_count) # stop the Loopingcall within the do_setup treatment + # We also added an initial delay of the actual session value + # since the initialization with threads is slower than + # the initial run of the looping call. drv.common.client.keep_session_loop.stop() @mock.patch.object(requests.Session, "request") @@ -2981,3 +2991,8 @@ class HBSDRESTFCDriverTest(test.TestCase): self.ctxt, TEST_VOLUME[0], new_type, diff, host) self.assertEqual(4, request.call_count) self.assertTrue(ret) + + def test_max_request_workers(self): + self.assertEqual(hbsd_rest_api._MAX_REQUEST_WORKERS, + self.driver.common.request_thread_pool_executor. + _max_workers) diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_iscsi.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_iscsi.py index 8df1f6d341c..3bca2b5cd55 100644 --- a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_iscsi.py +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_iscsi.py @@ -482,6 +482,11 @@ class HBSDRESTISCSIDriverTest(test.TestCase): group=conf.SHARED_CONF_GROUP) self.override_config('hitachi_host_mode_options', [], group=conf.SHARED_CONF_GROUP) + self.override_config('hitachi_rest_use_object_caching', False, + group=conf.SHARED_CONF_GROUP) + self.override_config('hitachi_rest_max_request_workers', + hbsd_rest_api._MAX_REQUEST_WORKERS, + group=conf.SHARED_CONF_GROUP) self.override_config('use_chap_auth', True, group=conf.SHARED_CONF_GROUP) @@ -620,6 +625,9 @@ class HBSDRESTISCSIDriverTest(test.TestCase): self.driver.create_export_snapshot(None, None, None) self.driver.remove_export_snapshot(None, None) # stop the Loopingcall within the do_setup treatment + # We also added an initial delay of the actual session value + # since the initialization with threads is slower than + # the initial run of the looping call. self.driver.common.client.keep_session_loop.stop() def tearDown(self): @@ -651,6 +659,9 @@ class HBSDRESTISCSIDriverTest(test.TestCase): self.assertEqual(1, brick_get_connector_properties.call_count) self.assertEqual(6, request.call_count) # stop the Loopingcall within the do_setup treatment + # We also added an initial delay of the actual session value + # since the initialization with threads is slower than + # the initial run of the looping call. drv.common.client.keep_session_loop.stop() @mock.patch.object(requests.Session, "request") diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_utils.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_utils.py new file mode 100644 index 00000000000..0f6fec9a98c --- /dev/null +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_utils.py @@ -0,0 +1,520 @@ +# Copyright (C) 2026, Hitachi Vantara +# +# 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. +# +"""Unit tests for Hitachi HBSD Driver Utilities.""" + +import ddt + +from cinder.tests.unit import test +from cinder.volume.drivers.hitachi import hbsd_utils + +SEARCHER_STORAGEID = '12345' + +SEARCHER_MISSINGGROUP_NAME = 'missinggroup' + +SEARCHER_GROUP3 = 3 +SEARCHER_GROUP7 = 7 +SEARCHER_GROUP3_WWNS = ['1000000000000000', '1000000000000001', + '1000000000000002', '1000000000000004', + '1000000000000005'] +SEARCHER_GROUP7_WWNS = ['1000000000000003'] + +SEARCHER_MYGROUP_NAME = 'mygroup' +SEARCHER_MYGROUP_WWNS = SEARCHER_GROUP7_WWNS +SEARCHER_MYGROUP_NUM = SEARCHER_GROUP7 + +SEARCHER_TEST_PORT = 'CL1-A' +SEARCHER_MISSING_WWNS = ['1000000000000075'] + +SEARCHER_META_DATA = "META" + +# Include 0 in all groups as it can have different behaviors with +# boolean conversions. +SEARCHER_ALL_GROUPS_AND_META = [(0, SEARCHER_META_DATA), + (SEARCHER_GROUP3, SEARCHER_META_DATA), + (SEARCHER_GROUP7, SEARCHER_META_DATA)] +SEARCHER_ALL_NAMES = [SEARCHER_MYGROUP_NAME, SEARCHER_MISSINGGROUP_NAME] +SEARCHER_ALL_VALID_WWNS = SEARCHER_GROUP3_WWNS + SEARCHER_GROUP7_WWNS +SEARCHER_ALL_VALID_NAMES = [SEARCHER_MYGROUP_NAME] + + +@ddt.ddt +class HBSDGroupSearcherTest(test.TestCase): + """Unit test class for HBSD utils.""" + + def setUp(self): + """Set up the test environment.""" + + super(HBSDGroupSearcherTest, self).setUp() + + def tearDown(self): + super(HBSDGroupSearcherTest, self).tearDown() + + class QueryObject(): + + def __init__(self): + self.group_target_lookup = 0 + self.group_name_lookup = 0 + self.all_group_lookup = 0 + + def query(self, port: str, group: int | str | + None) -> list[str] | tuple[int, list[str]] | list[int]: + + def _lookup_group_targets(port: str, groupNum: int): + targets = list() + if groupNum == SEARCHER_GROUP7: + targets = SEARCHER_GROUP7_WWNS + elif groupNum == SEARCHER_GROUP3: + targets = SEARCHER_GROUP3_WWNS + return targets + + def _lookup_group_by_name(port: str, group: str): + if group == SEARCHER_MYGROUP_NAME: + targets = SEARCHER_MYGROUP_WWNS + groupNum = SEARCHER_MYGROUP_NUM + return (groupNum, SEARCHER_META_DATA), targets + return None + + def _lookup_all_groups(port: str): + return SEARCHER_ALL_GROUPS_AND_META + + if isinstance(group, int): + self.group_target_lookup += 1 + return _lookup_group_targets(port, group) + elif isinstance(group, str): + self.group_name_lookup += 1 + return _lookup_group_by_name(port, group) + + self.all_group_lookup += 1 + return _lookup_all_groups(port) + + @ddt.data(hbsd_utils.HostConnectorSearcher(QueryObject().query), + hbsd_utils.CachingHostConnectorSearcher( + SEARCHER_STORAGEID, + QueryObject().query)) + def test_group_searcher(self, searcher): + + # Test that all of our searches return the exected result + # regardless of caching. + groupAndMeta = searcher.find(SEARCHER_TEST_PORT, + [SEARCHER_GROUP3_WWNS[4]], + list()) + self.assertEqual((SEARCHER_GROUP3, SEARCHER_META_DATA), groupAndMeta) + + groups = list() + groups.append(SEARCHER_MISSINGGROUP_NAME) + groupAndMeta = searcher.find(SEARCHER_TEST_PORT, + [SEARCHER_GROUP3_WWNS[2]], + groups) + self.assertEqual((SEARCHER_GROUP3, SEARCHER_META_DATA), groupAndMeta) + + groups = list() + groups.append(SEARCHER_MYGROUP_NAME) + groupAndMeta = searcher.find(SEARCHER_TEST_PORT, + [SEARCHER_GROUP7_WWNS[0]], + groups) + self.assertEqual((SEARCHER_GROUP7, SEARCHER_META_DATA), groupAndMeta) + + groups = list() + groups.append(SEARCHER_MISSINGGROUP_NAME) + groupAndMeta = searcher.find(SEARCHER_TEST_PORT, + [SEARCHER_GROUP7_WWNS[0]], + groups) + self.assertEqual((SEARCHER_GROUP7, SEARCHER_META_DATA), groupAndMeta) + + groups = list() + groups.append(SEARCHER_MYGROUP_NAME) + groupAndMeta = searcher.find(SEARCHER_TEST_PORT, + [SEARCHER_MISSING_WWNS[0]], + groups) + self.assertIsNone(groupAndMeta) + + groups = list() + groups.append(SEARCHER_MISSINGGROUP_NAME) + groupAndMeta = searcher.find(SEARCHER_TEST_PORT, + [SEARCHER_MISSING_WWNS[0]], + groups) + self.assertIsNone(groupAndMeta) + + groups = list() + groups.append(SEARCHER_MISSINGGROUP_NAME) + groups.append(SEARCHER_MYGROUP_NAME) + groupAndMeta = searcher.find(SEARCHER_TEST_PORT, + [SEARCHER_GROUP7_WWNS[0]], + groups) + self.assertEqual((SEARCHER_GROUP7, SEARCHER_META_DATA), groupAndMeta) + + is_caching = hasattr(searcher, '_connector_cache') + + if is_caching: + # Validate that all our items were cached as expected. + self.assertEqual(len(SEARCHER_ALL_VALID_WWNS), + len(searcher._connector_cache._target_cache)) + self.assertEqual(len(SEARCHER_ALL_VALID_NAMES), + len(searcher._connector_cache._group_name_cache)) + self.assertEqual(len(SEARCHER_ALL_GROUPS_AND_META), + len(searcher._connector_cache._group_cache)) + for wwn in SEARCHER_ALL_VALID_WWNS: + self.assertTrue( + searcher._connector_cache._generate_target_key( + SEARCHER_TEST_PORT, wwn) in + searcher._connector_cache._target_cache) + for name in SEARCHER_ALL_VALID_NAMES: + self.assertTrue( + searcher._connector_cache._generate_group_name_key( + SEARCHER_TEST_PORT, name) in + searcher._connector_cache._group_name_cache) + for groupAndMeta in SEARCHER_ALL_GROUPS_AND_META: + group, meta = groupAndMeta + self.assertTrue( + searcher._connector_cache._generate_group_key( + SEARCHER_TEST_PORT, group) in + searcher._connector_cache._group_cache) + self.assertEqual(SEARCHER_META_DATA, meta) + # Validate that the internal queries executed the expected + # number of times (negating cache hits). + self.assertEqual(2, + searcher._queryFunc.__self__.group_name_lookup) + self.assertEqual(2, + searcher._queryFunc.__self__.group_target_lookup) + self.assertEqual(3, + searcher._queryFunc.__self__.all_group_lookup) + else: + # Validate that the internal queries executed the expected + # number of times. + self.assertEqual(7, + searcher._queryFunc.__self__.group_name_lookup) + self.assertEqual(13, + searcher._queryFunc.__self__.group_target_lookup) + self.assertEqual(5, + searcher._queryFunc.__self__.all_group_lookup) + + # Test resetting the cache for group 3. + searcher.on_reset_group(SEARCHER_TEST_PORT, SEARCHER_GROUP3) + + if is_caching: + # Validate that the required items were removed from the cache. + self.assertEqual(len(SEARCHER_ALL_VALID_WWNS) - + len(SEARCHER_GROUP3_WWNS), + len(searcher._connector_cache._target_cache)) + self.assertEqual(len(SEARCHER_ALL_VALID_NAMES), + len(searcher._connector_cache._group_name_cache)) + self.assertEqual(len(SEARCHER_ALL_GROUPS_AND_META) - 1, + len(searcher._connector_cache._group_cache)) + for wwn in SEARCHER_ALL_VALID_WWNS: + if wwn in SEARCHER_GROUP3_WWNS: + self.assertFalse( + searcher._connector_cache._generate_target_key( + SEARCHER_TEST_PORT, wwn) in + searcher._connector_cache._target_cache) + else: + self.assertTrue( + searcher._connector_cache._generate_target_key( + SEARCHER_TEST_PORT, wwn) in + searcher._connector_cache._target_cache) + for name in SEARCHER_ALL_VALID_NAMES: + self.assertTrue( + searcher._connector_cache._generate_group_name_key( + SEARCHER_TEST_PORT, name) in + searcher._connector_cache._group_name_cache) + for groupAndMeta in SEARCHER_ALL_GROUPS_AND_META: + group, meta = groupAndMeta + if group == SEARCHER_GROUP3: + self.assertFalse( + searcher._connector_cache._generate_group_key( + SEARCHER_TEST_PORT, group) in + searcher._connector_cache._group_cache) + else: + self.assertTrue( + searcher._connector_cache._generate_group_key( + SEARCHER_TEST_PORT, group) in + searcher._connector_cache._group_cache) + + # Re-find our WWN by group and check the lookups. + group = searcher.find(SEARCHER_TEST_PORT, [SEARCHER_GROUP3_WWNS[0]], + list()) + self.assertEqual((SEARCHER_GROUP3, SEARCHER_META_DATA), group) + + if is_caching: + self.assertEqual(2, + searcher._queryFunc.__self__.group_name_lookup) + self.assertEqual(3, + searcher._queryFunc.__self__.group_target_lookup) + self.assertEqual(4, + searcher._queryFunc.__self__.all_group_lookup) + else: + self.assertEqual(7, + searcher._queryFunc.__self__.group_name_lookup) + self.assertEqual(15, + searcher._queryFunc.__self__.group_target_lookup) + self.assertEqual(6, + searcher._queryFunc.__self__.all_group_lookup) + + # Reset entire cache and validate that it is cleared. + searcher.on_reset() + if is_caching: + self.assertEqual(0, len(searcher._connector_cache._target_cache)) + self.assertEqual(0, len(searcher._connector_cache._group_cache)) + self.assertEqual(0, + len(searcher._connector_cache._group_name_cache)) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_generate_target_key(self, cache): + self.assertEqual("PORT\tWWN", + cache._generate_target_key("PORT", "WWN")) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_generate_group_key(self, cache): + self.assertEqual("PORT\t1", + cache._generate_group_key("PORT", 1)) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_generate_group_name_key(self, cache): + self.assertEqual("PORT\tNAME", + cache._generate_group_name_key("PORT", "NAME")) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_lookup_cache_empty(self, cache): + self.assertIsNone(cache.lookup("PORT", "WWN")) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_lookup_not_cached(self, cache): + cache.cache("PORT2", (1, None), None, ["WWN2"]) + self.assertIsNone(cache.lookup("PORT", "WWN")) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_lookup_cached_unnamed(self, cache): + cache.cache("PORT", (1, None), None, ["WWN"]) + self.assertEqual((1, None), cache.lookup("PORT", "WWN")) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_lookup_cached_named(self, cache): + cache.cache("PORT", (1, None), "NAME", ["WWN"]) + self.assertEqual((1, None), cache.lookup("PORT", "WWN")) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_lookup_cached_with_meta(self, cache): + cache.cache("PORT", (1, 3), None, ["WWN"]) + self.assertEqual((1, 3), cache.lookup("PORT", "WWN")) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_is_group_cached(self, cache): + self.assertFalse(cache.is_group_cached("PORT", 1)) + cache.cache("PORT", (1, None), None, ["WWN"]) + self.assertTrue(cache.is_group_cached("PORT", 1)) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_is_group_name_cached(self, cache): + self.assertFalse(cache.is_group_name_cached("PORT", "NAME")) + cache.cache("PORT", (1, None), "NAME", ["WWN"]) + self.assertTrue(cache.is_group_name_cached("PORT", "NAME")) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_multi_port(self, cache): + cache.cache("PORT", (1, None), None, ["WWN"]) + cache.cache("PORT2", (1, None), None, ["WWN"]) + cache.cache("PORT3", (1, None), None, []) + self.assertEqual(3, len(cache._group_cache)) + self.assertEqual(2, len(cache._target_cache)) + self.assertEqual(0, len(cache._group_name_cache)) + self.assertTrue( + cache._generate_group_key("PORT", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_key("PORT2", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT2", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_key("PORT3", 1) in cache._group_cache) + self.assertFalse( + cache._generate_target_key("PORT3", "WWN") in cache._target_cache) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_multi_port_with_name(self, cache): + cache.cache("PORT", (1, None), "NAME", ["WWN"]) + cache.cache("PORT2", (1, None), "NAME", ["WWN"]) + cache.cache("PORT3", (1, None), "NAME", []) + self.assertEqual(3, len(cache._group_cache)) + self.assertEqual(2, len(cache._target_cache)) + self.assertEqual(3, len(cache._group_name_cache)) + self.assertTrue( + cache._generate_group_key("PORT", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT", "NAME") in + cache._group_name_cache) + self.assertTrue( + cache._generate_group_key("PORT2", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT2", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT2", "NAME") in + cache._group_name_cache) + self.assertTrue( + cache._generate_group_key("PORT3", 1) in cache._group_cache) + self.assertFalse( + cache._generate_target_key("PORT3", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT3", "NAME") in + cache._group_name_cache) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_multi_wwn(self, cache): + cache.cache("PORT", (1, None), None, ["WWN", "WWN2", "WWN3"]) + self.assertEqual(1, len(cache._group_cache)) + self.assertEqual(3, len(cache._target_cache)) + self.assertEqual(0, len(cache._group_name_cache)) + self.assertTrue( + cache._generate_group_key("PORT", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN2") in cache._target_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN3") in cache._target_cache) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_multi_wwn_with_name(self, cache): + cache.cache("PORT", (1, None), "NAME", ["WWN", "WWN2", "WWN3"]) + self.assertEqual(1, len(cache._group_cache)) + self.assertEqual(3, len(cache._target_cache)) + self.assertEqual(1, len(cache._group_name_cache)) + self.assertTrue( + cache._generate_group_key("PORT", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN2") in cache._target_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN3") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT", "NAME") in + cache._group_name_cache) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_no_wwn(self, cache): + cache.cache("PORT", (1, None), None, []) + self.assertEqual(1, len(cache._group_cache)) + self.assertEqual(0, len(cache._target_cache)) + self.assertEqual(0, len(cache._group_name_cache)) + self.assertTrue( + cache._generate_group_key("PORT", 1) in cache._group_cache) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_no_wwn_with_name(self, cache): + cache.cache("PORT", (1, None), "NAME", []) + self.assertEqual(1, len(cache._group_cache)) + self.assertEqual(0, len(cache._target_cache)) + self.assertEqual(1, len(cache._group_name_cache)) + self.assertTrue( + cache._generate_group_key("PORT", 1) in cache._group_cache) + self.assertTrue( + cache._generate_group_name_key("PORT", "NAME") in + cache._group_name_cache) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_clear_empty(self, cache): + cache.clear() + self.assertEqual(0, len(cache._group_cache)) + self.assertEqual(0, len(cache._target_cache)) + self.assertEqual(0, len(cache._group_name_cache)) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_clear(self, cache): + cache.cache("PORT", (1, None), "NAME", ["WWN"]) + cache.cache("PORT2", (1, None), "NAME", ["WWN"]) + cache.cache("PORT3", (1, None), "NAME", []) + cache.clear() + self.assertEqual(0, len(cache._group_cache)) + self.assertEqual(0, len(cache._target_cache)) + self.assertEqual(0, len(cache._group_name_cache)) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_clear_group_empty(self, cache): + cache.clear_group("PORT", 1) + self.assertEqual(0, len(cache._group_cache)) + self.assertEqual(0, len(cache._target_cache)) + self.assertEqual(0, len(cache._group_name_cache)) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_clear_group_not_found(self, cache): + cache.cache("PORT", (1, None), "NAME", ["WWN"]) + cache.cache("PORT2", (1, None), "NAME", ["WWN"]) + cache.cache("PORT3", (1, None), "NAME", []) + cache.clear_group("PORT", 7) + self.assertEqual(3, len(cache._group_cache)) + self.assertEqual(2, len(cache._target_cache)) + self.assertEqual(3, len(cache._group_name_cache)) + self.assertTrue( + cache._generate_group_key("PORT", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT", "NAME") in + cache._group_name_cache) + self.assertTrue( + cache._generate_group_key("PORT2", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT2", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT2", "NAME") in + cache._group_name_cache) + self.assertTrue( + cache._generate_group_key("PORT3", 1) in cache._group_cache) + self.assertFalse( + cache._generate_target_key("PORT3", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT3", "NAME") in + cache._group_name_cache) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_clear_group(self, cache): + cache.cache("PORT", (1, None), "NAME", ["WWN"]) + cache.clear_group("PORT", 1) + self.assertEqual(0, len(cache._group_cache)) + self.assertEqual(0, len(cache._target_cache)) + self.assertEqual(0, len(cache._group_name_cache)) + + @ddt.data(hbsd_utils.ConnectorSearcherCache()) + def test_cache_clear_1_group(self, cache): + cache.cache("PORT", (1, None), "NAME", ["WWN"]) + cache.cache("PORT2", (1, None), "NAME", ["WWN"]) + cache.cache("PORT3", (1, None), "NAME", []) + cache.clear_group("PORT", 1) + self.assertEqual(2, len(cache._group_cache)) + self.assertEqual(1, len(cache._target_cache)) + self.assertEqual(2, len(cache._group_name_cache)) + self.assertFalse( + cache._generate_group_key("PORT", 1) in cache._group_cache) + self.assertFalse( + cache._generate_target_key("PORT", "WWN") in cache._target_cache) + self.assertFalse( + cache._generate_group_name_key("PORT", "NAME") in + cache._group_name_cache) + self.assertTrue( + cache._generate_group_key("PORT2", 1) in cache._group_cache) + self.assertTrue( + cache._generate_target_key("PORT2", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT2", "NAME") in + cache._group_name_cache) + self.assertTrue( + cache._generate_group_key("PORT3", 1) in cache._group_cache) + self.assertFalse( + cache._generate_target_key("PORT3", "WWN") in cache._target_cache) + self.assertTrue( + cache._generate_group_name_key("PORT3", "NAME") in + cache._group_name_cache) diff --git a/cinder/tests/unit/volume/drivers/hpe/xp/test_hpe_xp_rest_fc.py b/cinder/tests/unit/volume/drivers/hpe/xp/test_hpe_xp_rest_fc.py index 6060d61931f..ab4fe9db4ed 100644 --- a/cinder/tests/unit/volume/drivers/hpe/xp/test_hpe_xp_rest_fc.py +++ b/cinder/tests/unit/volume/drivers/hpe/xp/test_hpe_xp_rest_fc.py @@ -503,6 +503,8 @@ class HPEXPRESTFCDriverTest(test.TestCase): self.configuration.hpexp_rest_tcp_keepcnt = ( hbsd_rest_api._TCP_KEEPCNT) self.configuration.hpexp_host_mode_options = [] + self.configuration.hpexp_rest_use_object_caching = False + self.configuration.hpexp_rest_max_request_workers = 8 self.configuration.hpexp_zoning_request = False diff --git a/cinder/tests/unit/volume/drivers/hpe/xp/test_hpe_xp_rest_iscsi.py b/cinder/tests/unit/volume/drivers/hpe/xp/test_hpe_xp_rest_iscsi.py index 797f8668851..bd2f91611f3 100644 --- a/cinder/tests/unit/volume/drivers/hpe/xp/test_hpe_xp_rest_iscsi.py +++ b/cinder/tests/unit/volume/drivers/hpe/xp/test_hpe_xp_rest_iscsi.py @@ -399,6 +399,8 @@ class HPEXPRESTISCSIDriverTest(test.TestCase): self.configuration.hpexp_rest_tcp_keepcnt = ( hbsd_rest_api._TCP_KEEPCNT) self.configuration.hpexp_host_mode_options = [] + self.configuration.hpexp_rest_use_object_caching = False + self.configuration.hpexp_rest_max_request_workers = 8 self.configuration.use_chap_auth = True self.configuration.chap_username = CONFIG_MAP['auth_user'] diff --git a/cinder/tests/unit/volume/drivers/nec/v/test_internal_nec_rest_fc.py b/cinder/tests/unit/volume/drivers/nec/v/test_internal_nec_rest_fc.py index 97e72c9d203..f2941566780 100644 --- a/cinder/tests/unit/volume/drivers/nec/v/test_internal_nec_rest_fc.py +++ b/cinder/tests/unit/volume/drivers/nec/v/test_internal_nec_rest_fc.py @@ -496,6 +496,9 @@ class VStorageRESTFCDriverTest(test.TestCase): self.configuration.nec_v_rest_tcp_keepcnt = ( hbsd_rest_api._TCP_KEEPCNT) self.configuration.nec_v_host_mode_options = [] + self.configuration.nec_v_rest_use_object_caching = False + self.configuration.nec_v_rest_max_request_workers = ( + hbsd_rest_api._MAX_REQUEST_WORKERS) self.configuration.nec_v_zoning_request = False diff --git a/cinder/tests/unit/volume/drivers/nec/v/test_internal_nec_rest_iscsi.py b/cinder/tests/unit/volume/drivers/nec/v/test_internal_nec_rest_iscsi.py index c2dafef38d0..79bfdfec84e 100644 --- a/cinder/tests/unit/volume/drivers/nec/v/test_internal_nec_rest_iscsi.py +++ b/cinder/tests/unit/volume/drivers/nec/v/test_internal_nec_rest_iscsi.py @@ -408,6 +408,9 @@ class VStorageRESTISCSIDriverTest(test.TestCase): self.configuration.nec_v_rest_tcp_keepcnt = ( hbsd_rest_api._TCP_KEEPCNT) self.configuration.nec_v_host_mode_options = [] + self.configuration.nec_v_rest_use_object_caching = False + self.configuration.nec_v_rest_max_request_workers = ( + hbsd_rest_api._MAX_REQUEST_WORKERS) self.configuration.use_chap_auth = True self.configuration.chap_username = CONFIG_MAP['auth_user'] diff --git a/cinder/tests/unit/volume/drivers/nec/v/test_nec_rest_fc.py b/cinder/tests/unit/volume/drivers/nec/v/test_nec_rest_fc.py index 4f753e315ae..eed37b92655 100644 --- a/cinder/tests/unit/volume/drivers/nec/v/test_nec_rest_fc.py +++ b/cinder/tests/unit/volume/drivers/nec/v/test_nec_rest_fc.py @@ -224,6 +224,8 @@ class VStorageRESTFCDriverTest(test.TestCase): self.configuration.nec_v_rest_tcp_keepcnt = ( hbsd_rest_api._TCP_KEEPCNT) self.configuration.nec_v_host_mode_options = [] + self.configuration.nec_v_rest_use_object_caching = False + self.configuration.nec_v_rest_max_request_workers = 8 self.configuration.nec_v_zoning_request = False diff --git a/cinder/tests/unit/volume/drivers/nec/v/test_nec_rest_iscsi.py b/cinder/tests/unit/volume/drivers/nec/v/test_nec_rest_iscsi.py index c2c19d623ad..6c491bc9ae7 100644 --- a/cinder/tests/unit/volume/drivers/nec/v/test_nec_rest_iscsi.py +++ b/cinder/tests/unit/volume/drivers/nec/v/test_nec_rest_iscsi.py @@ -246,6 +246,8 @@ class VStorageRESTISCSIDriverTest(test.TestCase): self.configuration.nec_v_rest_tcp_keepcnt = ( hbsd_rest_api._TCP_KEEPCNT) self.configuration.nec_v_host_mode_options = [] + self.configuration.nec_v_rest_use_object_caching = False + self.configuration.nec_v_rest_max_request_workers = 8 self.configuration.use_chap_auth = True self.configuration.chap_username = CONFIG_MAP['auth_user'] diff --git a/cinder/volume/drivers/hitachi/hbsd_fc.py b/cinder/volume/drivers/hitachi/hbsd_fc.py index 4948ce3f0eb..c4d4b966dfa 100644 --- a/cinder/volume/drivers/hitachi/hbsd_fc.py +++ b/cinder/volume/drivers/hitachi/hbsd_fc.py @@ -88,6 +88,7 @@ class HBSDFCDriver(driver.FibreChannelDriver): 2.6.1 - Support extending volume with snapshot. 2.7.0 - Support adaptive QoS upperIops setting. 2.7.1 - Support GAD coexisting with ADR. + 2.7.2 - Add caching/batching to fix severe performance issues. """ diff --git a/cinder/volume/drivers/hitachi/hbsd_iscsi.py b/cinder/volume/drivers/hitachi/hbsd_iscsi.py index 77cd0fc8d7f..e9d8b87b70d 100644 --- a/cinder/volume/drivers/hitachi/hbsd_iscsi.py +++ b/cinder/volume/drivers/hitachi/hbsd_iscsi.py @@ -88,6 +88,7 @@ class HBSDISCSIDriver(driver.ISCSIDriver): 2.6.1 - Support extending volume with snapshot. 2.7.0 - Support adaptive QoS upperIops setting. 2.7.1 - Support GAD coexisting with ADR. + 2.7.2 - Add caching/batching to fix severe performance issues. """ diff --git a/cinder/volume/drivers/hitachi/hbsd_rest.py b/cinder/volume/drivers/hitachi/hbsd_rest.py index 681dfdd77e8..7478cac7e9a 100644 --- a/cinder/volume/drivers/hitachi/hbsd_rest.py +++ b/cinder/volume/drivers/hitachi/hbsd_rest.py @@ -16,6 +16,7 @@ """REST interface module for Hitachi HBSD Driver.""" from collections import defaultdict +import concurrent.futures import json import re @@ -215,6 +216,15 @@ REST_VOLUME_OPTS = [ item_type=types.Integer(), default=[], help='Host mode option for host group or iSCSI target.'), + cfg.BoolOpt( + 'hitachi_rest_use_object_caching', + default=True, + help='Set True to enable object caching of certain REST objects ' + 'for better performance.'), + cfg.IntOpt( + 'hitachi_rest_max_request_workers', + default=rest_api._MAX_REQUEST_WORKERS, + help='The maximum number of workers for concurrent requests.'), ] REST_PAIR_OPTS = [ @@ -305,6 +315,28 @@ class HBSDREST(common.HBSDCommon): self.client = None + self.request_thread_pool_executor = \ + concurrent.futures.ThreadPoolExecutor( + max_workers=self.conf.safe_get( + self.driver_info['param_prefix'] + + '_rest_max_request_workers')) + + self.connector_searcher = None + if self.conf.safe_get(self.driver_info['param_prefix'] + + '_rest_use_object_caching'): + self.connector_searcher = utils.CachingHostConnectorSearcher( + self.conf.safe_get(self.driver_info['param_prefix'] + + '_storage_id'), + self.connector_searcher_query) + else: + self.connector_searcher = utils.HostConnectorSearcher( + self.connector_searcher_query) + + def __del__(self): + """Shut down the driver.""" + self.request_thread_pool_executor.shutdown(wait=False, + cancel_futures=True) + def do_setup(self, context): if hasattr( self.conf, @@ -337,6 +369,7 @@ class HBSDREST(common.HBSDCommon): self.conf.san_login, self.conf.san_password, self.driver_info['driver_prefix'], + self.connector_searcher, tcp_keepalive=self.conf.hitachi_rest_tcp_keepalive, verify=verify, is_rep=is_rep) @@ -351,6 +384,11 @@ class HBSDREST(common.HBSDCommon): if self.client is not None: self.client.enter_keep_session() + def connector_searcher_query(self, port: str, group: int | str | None): + # See hbsd_utils.HostConnectorSearcher for a description of + # this method behavior. + pass + def _set_dr_mode(self, body, capacity_saving): dr_mode = _CAPACITY_SAVING_DR_MODE.get(capacity_saving) if not dr_mode: @@ -845,18 +883,31 @@ class HBSDREST(common.HBSDCommon): port, gid = targets['list'][0] lun = self._run_add_lun(ldev, port, gid) targets['lun'][port] = True + + def _worker(ldev, port, gid, lun): + try: + lun = self._run_add_lun(ldev, port, gid, lun=lun) + return port + except exception.VolumeDriverException: + self.output_log(MSG.MAP_LDEV_FAILED, ldev=ldev, + port=port, id=gid, lun=lun) + return None + + futures = [] for port, gid in targets['list'][head:]: # When multipath is configured, Nova compute expects that # target_lun define the same value in all storage target. # Therefore, it should use same value of lun in other target. - try: - lun2 = self._run_add_lun(ldev, port, gid, lun=lun) - if lun2 is not None: - targets['lun'][port] = True - raise_err = False - except exception.VolumeDriverException: - self.output_log(MSG.MAP_LDEV_FAILED, ldev=ldev, - port=port, id=gid, lun=lun) + future = self.request_thread_pool_executor.submit( + _worker, ldev, port, gid, lun) + futures.append(future) + + for future in futures: + result = future.result() + if result is not None: + targets['lun'][result] = True + raise_err = False + if raise_err: msg = self.output_log( MSG.CONNECT_VOLUME_FAILED, @@ -910,19 +961,37 @@ class HBSDREST(common.HBSDCommon): ignore_return_code = [EX_ENOOBJ] ignore_message_id = [rest_api.MSGID_SPECIFIED_OBJECT_DOES_NOT_EXIST] timeout = self.conf.hitachi_state_transition_timeout + + def _worker(port, gid, lun): + try: + self.client.delete_lun(port, gid, lun, + interval=interval, + ignore_return_code=ignore_return_code, + ignore_message_id=ignore_message_id, + timeout=timeout) + LOG.debug( + 'Deleted logical unit path of the specified logical ' + 'device. (LDEV: %(ldev)s, host group: %(gid)s)', + {'ldev': ldev, 'gid': gid}) + return None + except Exception as ex: + return ex + + futures = [] for target in targets['list']: port = target['portId'] gid = target['hostGroupNumber'] lun = target['lun'] - self.client.delete_lun(port, gid, lun, - interval=interval, - ignore_return_code=ignore_return_code, - ignore_message_id=ignore_message_id, - timeout=timeout) - LOG.debug( - 'Deleted logical unit path of the specified logical ' - 'device. (LDEV: %(ldev)s, host group: %(target)s)', - {'ldev': ldev, 'target': target}) + future = self.request_thread_pool_executor.submit( + _worker, port, gid, lun) + futures.append(future) + + # If we failed any of our operations with an exception, throw the + # first one encountered now. + for future in futures: + result = future.result() + if result is not None: + raise result def _get_target_luns(self, target): """Get the LUN mapping information of the host group.""" diff --git a/cinder/volume/drivers/hitachi/hbsd_rest_api.py b/cinder/volume/drivers/hitachi/hbsd_rest_api.py index 816e5d51822..21e9d432c17 100644 --- a/cinder/volume/drivers/hitachi/hbsd_rest_api.py +++ b/cinder/volume/drivers/hitachi/hbsd_rest_api.py @@ -46,6 +46,7 @@ _REST_SERVER_ERROR_TIMEOUT = 10 * 60 _KEEP_SESSION_LOOP_INTERVAL = 3 * 60 _ANOTHER_LDEV_MAPPED_RETRY_TIMEOUT = 10 * 60 _LOCK_RESOURCE_GROUP_TIMEOUT = 3 * 60 +_MAX_REQUEST_WORKERS = 8 _TCP_KEEPIDLE = 60 _TCP_KEEPINTVL = 15 @@ -235,10 +236,11 @@ class ResponseData(dict): class RestApiClient(): def __init__(self, conf, ip_addr, ip_port, storage_device_id, - user_id, user_pass, driver_prefix, tcp_keepalive=False, - verify=False, is_rep=False): + user_id, user_pass, driver_prefix, connector_searcher, + tcp_keepalive=False, verify=False, is_rep=False): """Initialize instance variables.""" self.conf = conf + self.connector_searcher = connector_searcher self.ip_addr = ip_addr self.ip_port = ip_port self.storage_id = storage_device_id @@ -600,7 +602,8 @@ class RestApiClient(): def enter_keep_session(self): """Begin the keeping of a session.""" self.keep_session_loop.start( - self.conf.hitachi_rest_keep_session_loop_interval) + self.conf.hitachi_rest_keep_session_loop_interval, + initial_delay=self.conf.hitachi_rest_keep_session_loop_interval) LOG.debug('enter_keep_session') @volume_utils.trace @@ -748,6 +751,7 @@ class RestApiClient(): 'number': host_group_number, } self._delete_object(url) + self.connector_searcher.on_reset_group(port_id, host_group_number) def modify_host_grp(self, port_id, host_group_number, body, **kwargs): """Modify a host group information.""" @@ -781,7 +785,9 @@ class RestApiClient(): } body = {"hostWwn": host_wwn, "portId": port_id, "hostGroupNumber": host_group_number} - return self._add_object(url, body=body, **kwargs)[0] + ret = self._add_object(url, body=body, **kwargs)[0] + self.connector_searcher.on_reset_group(port_id, host_group_number) + return ret def get_hba_iscsis(self, port_id, host_group_number): """Get a list of ISCSI information.""" @@ -806,7 +812,9 @@ class RestApiClient(): } body = {"iscsiName": iscsi_name, "portId": port_id, "hostGroupNumber": host_group_number} - return self._add_object(url, body=body)[0] + ret = self._add_object(url, body=body)[0] + self.connector_searcher.on_reset_group(port_id, host_group_number) + return ret def get_luns(self, port_id, host_group_number, is_basic_lun_information=False): diff --git a/cinder/volume/drivers/hitachi/hbsd_rest_fc.py b/cinder/volume/drivers/hitachi/hbsd_rest_fc.py index 5671e38f75a..852fd5b1aad 100644 --- a/cinder/volume/drivers/hitachi/hbsd_rest_fc.py +++ b/cinder/volume/drivers/hitachi/hbsd_rest_fc.py @@ -54,6 +54,44 @@ class HBSDRESTFC(rest.HBSDREST): super(HBSDRESTFC, self).__init__(conf, storage_protocol, db) self._lookup_service = fczm_utils.create_lookup_service() + def connector_searcher_query(self, port: str, group: int | str | None): + # See hbsd_utils.HostConnectorSearcher for a description of + # this method behavior. + + def _lookup_group_targets(port: str, groupNum: int) -> list[str]: + # Returns list of WWNs/targets. + + targets = [] + hba_wwns = self.client.get_hba_wwns(port, groupNum) + if hba_wwns: + targets = [hba_wwn['hostWwn'] for hba_wwn in hba_wwns] + return targets + + def _lookup_group_by_name(port: str, + group: str) -> tuple[int, list[str]]: + # Returns tuple of group number/metadata tuple and targets. + # None if group not found (or no targets in group). + hba_wwns = self.client.get_hba_wwns_by_name(port, group) + if hba_wwns: + gid = hba_wwns[0]['hostGroupNumber'] + targets = [hba_wwn['hostWwn'] for hba_wwn in hba_wwns] + return (gid, None), targets + + return None + + def _lookup_all_groups(port: str) -> list[int]: + # Returns list of groups by int. + params = {'portId': port} + host_grp_list = self.client.get_host_grps(params) + return [(data['hostGroupNumber'], None) for data in host_grp_list] + + if isinstance(group, int): + return _lookup_group_targets(port, group) + elif isinstance(group, str): + return _lookup_group_by_name(port, group) + + return _lookup_all_groups(port) + def connect_storage(self): """Prepare for using the storage.""" target_ports = self.conf.hitachi_target_ports @@ -191,56 +229,6 @@ class HBSDRESTFC(rest.HBSDREST): body['hostModeOptions'].append(int(opt)) self.client.modify_host_grp(port, gid, body, ignore_all_errors=True) - def _get_hwwns_in_hostgroup(self, port, gid, wwpns): - """Return WWN registered with the host group.""" - hwwns_in_hostgroup = [] - for hba_wwn in self.client.get_hba_wwns(port, gid): - hwwn = hba_wwn['hostWwn'] - if hwwn in wwpns: - hwwns_in_hostgroup.append(hwwn) - return hwwns_in_hostgroup - - def _set_target_info(self, targets, host_grps, wwpns): - """Set the information of the host group having the specified WWN.""" - for host_grp in host_grps: - port = host_grp['portId'] - gid = host_grp['hostGroupNumber'] - hwwns_in_hostgroup = self._get_hwwns_in_hostgroup(port, gid, wwpns) - if hwwns_in_hostgroup: - targets['info'][port] = True - targets['list'].append((port, gid)) - LOG.debug( - 'Found wwpns in host group. (port: %(port)s, ' - 'gid: %(gid)s, wwpns: %(wwpns)s)', - {'port': port, 'gid': gid, 'wwpns': hwwns_in_hostgroup}) - return True - return False - - def _get_hwwns_in_hostgroup_by_name(self, port, host_group_name, wwpns): - """Return WWN registered with the host group of the specified name.""" - hba_wwns = self.client.get_hba_wwns_by_name(port, host_group_name) - return [hba_wwn for hba_wwn in hba_wwns if hba_wwn['hostWwn'] in wwpns] - - def _set_target_info_by_names(self, targets, port, target_names, wwpns): - """Set the information of the host group having the specified name and - - the specified WWN. - """ - for target_name in target_names: - hwwns_in_hostgroup = self._get_hwwns_in_hostgroup_by_name( - port, target_name, wwpns) - if hwwns_in_hostgroup: - gid = hwwns_in_hostgroup[0]['hostGroupNumber'] - targets['info'][port] = True - targets['list'].append((port, gid)) - LOG.debug( - 'Found wwpns in host group. (port: %(port)s, ' - 'gid: %(gid)s, wwpns: %(wwpns)s)', - {'port': port, 'gid': gid, 'wwpns': - [hwwn['hostWwn'] for hwwn in hwwns_in_hostgroup]}) - return True - return False - def find_targets_from_storage( self, targets, connector, target_ports): """Find mapped ports, memorize them and return unmapped port count.""" @@ -253,19 +241,39 @@ class HBSDRESTFC(rest.HBSDREST): 'ip': connector['ip'], } ) - not_found_count = 0 + + def _worker(port, wwpns, target_names): + try: + groupAndMeta = self.connector_searcher.find(port, wwpns, + target_names) + if groupAndMeta is not None: + group, meta = groupAndMeta + return (port, group, meta) + return None + except Exception as ex: + return ex + + futures = [] for port in target_ports: targets['info'][port] = False - if self._set_target_info_by_names( - targets, port, target_names, wwpns): - continue - host_grps = self.client.get_host_grps({'portId': port}) - if self._set_target_info( - targets, [hg for hg in host_grps if hg['hostGroupName'] not in - target_names], wwpns): - pass - else: + future = self.request_thread_pool_executor.submit( + _worker, port, wwpns, target_names) + futures.append(future) + + not_found_count = 0 + appended_set = set() + for future in futures: + result = future.result() + if result is None: not_found_count += 1 + elif isinstance(result, Exception): + raise result + else: + port, group, _ = result + targets['info'][port] = True + if (port, group) not in appended_set: + targets['list'].append((port, group)) + appended_set.add((port, group)) if self.get_port_scheduler_param(): """ diff --git a/cinder/volume/drivers/hitachi/hbsd_rest_iscsi.py b/cinder/volume/drivers/hitachi/hbsd_rest_iscsi.py index 546dfc3a9c9..18d2036cf19 100644 --- a/cinder/volume/drivers/hitachi/hbsd_rest_iscsi.py +++ b/cinder/volume/drivers/hitachi/hbsd_rest_iscsi.py @@ -43,6 +43,46 @@ class HBSDRESTISCSI(rest.HBSDREST): } return True, ipv4_addr, tcp_port + def connector_searcher_query(self, port: str, group: int | str | None): + # See hbsd_utils.HostConnectorSearcher for a description of + # this method behavior. + + def _lookup_group_targets(port: str, groupNum: int) -> list[str]: + # Returns list of WWNs/targets. + targets = [] + + hba_iscsis = self.client.get_hba_iscsis(port, groupNum) + if hba_iscsis: + targets = [hba_iscsi['iscsiName'] for hba_iscsi in hba_iscsis] + return targets + + def _lookup_group_by_name(port: str, + group: str) -> tuple[int, list[str]]: + # Returns tuple of group number/metadata tuple and targets. + # None if group not found (or no targets in group). + hba_iscsis = self.client.get_hba_iscsis_by_name(port, group) + if hba_iscsis: + gid = hba_iscsis[0]['hostGroupNumber'] + targets = [hba_iscsi['iscsiName'] for hba_iscsi in hba_iscsis] + storage_iqn = self.client.get_host_grp(port, gid)['iscsiName'] + return (gid, {'iscsiName': storage_iqn}), targets + return None + + def _lookup_all_groups(port: str) -> list[int]: + # Returns list of groups by int. + params = {'portId': port} + host_grp_list = self.client.get_host_grps(params) + return [(data['hostGroupNumber'], + {'iscsiName': data['iscsiName']}) for + data in host_grp_list] + + if isinstance(group, int): + return _lookup_group_targets(port, group) + elif isinstance(group, str): + return _lookup_group_by_name(port, group) + + return _lookup_all_groups(port) + def connect_storage(self): """Prepare for using the storage.""" target_ports = self.conf.hitachi_target_ports @@ -143,73 +183,50 @@ class HBSDRESTISCSI(rest.HBSDREST): body['hostModeOptions'].append(int(opt)) self.client.modify_host_grp(port, gid, body) - def _is_host_iqn_registered_in_target(self, port, gid, host_iqn): - """Check if the specified IQN is registered with iSCSI target.""" - for hba_iscsi in self.client.get_hba_iscsis(port, gid): - if host_iqn == hba_iscsi['iscsiName']: - return True - return False - - def _set_target_info(self, targets, host_grps, iqn): - """Set the information of the iSCSI target having the specified IQN.""" - for host_grp in host_grps: - port = host_grp['portId'] - gid = host_grp['hostGroupNumber'] - storage_iqn = host_grp['iscsiName'] - if self._is_host_iqn_registered_in_target(port, gid, iqn): - targets['info'][port] = True - targets['list'].append((port, gid)) - targets['iqns'][(port, gid)] = storage_iqn - return True - return False - - def _get_host_iqn_registered_in_target_by_name( - self, port, target_name, host_iqn): - """Get the information of the iSCSI target having the specified name - - and the specified IQN. - """ - for hba_iscsi in self.client.get_hba_iscsis_by_name(port, target_name): - if host_iqn == hba_iscsi['iscsiName']: - return hba_iscsi - return None - - def _set_target_info_by_name(self, targets, port, target_name, iqn): - """Set the information of the iSCSI target having the specified name - - and the specified IQN. - """ - host_iqn_registered_in_target = ( - self._get_host_iqn_registered_in_target_by_name( - port, target_name, iqn)) - if host_iqn_registered_in_target: - gid = host_iqn_registered_in_target['hostGroupNumber'] - storage_iqn = self.client.get_host_grp(port, gid)['iscsiName'] - targets['info'][port] = True - targets['list'].append((port, gid)) - targets['iqns'][(port, gid)] = storage_iqn - return True - return False - def find_targets_from_storage(self, targets, connector, target_ports): + """Find mapped ports, memorize them and return unmapped port count.""" + iqn = self.get_hba_ids_from_connector(connector) - not_found_count = 0 + target_names = [] + if 'ip' in connector: + target_names.append(self.create_target_name(connector)) + + def _worker(port, iqn, target_names): + try: + groupAndMeta = self.connector_searcher.find(port, [iqn], + target_names) + if groupAndMeta is not None: + group, meta = groupAndMeta + return (port, group, meta) + return None + except Exception as ex: + return ex + + futures = [] for port in target_ports: targets['info'][port] = False - if 'ip' in connector: - target_name = self.create_target_name(connector) - if self._set_target_info_by_name( - targets, port, target_name, iqn): - continue - host_grps = self.client.get_host_grps({'portId': port}) - if 'ip' in connector: - host_grps = [hg for hg in host_grps - if hg['hostGroupName'] != target_name] - if self._set_target_info(targets, host_grps, iqn): - pass - else: + future = self.request_thread_pool_executor.submit( + _worker, port, iqn, target_names) + futures.append(future) + + not_found_count = 0 + appended_set = set() + for future in futures: + result = future.result() + if result is None: not_found_count += 1 + elif isinstance(result, Exception): + raise result + else: + port, group, meta = result + storage_iqn = meta['iscsiName'] + targets['info'][port] = True + if (port, group) not in appended_set: + targets['list'].append((port, group)) + appended_set.add((port, group)) + targets['iqns'][(port, group)] = storage_iqn + return not_found_count def initialize_connection( diff --git a/cinder/volume/drivers/hitachi/hbsd_utils.py b/cinder/volume/drivers/hitachi/hbsd_utils.py index 30e5b5e1352..e080f6879ff 100644 --- a/cinder/volume/drivers/hitachi/hbsd_utils.py +++ b/cinder/volume/drivers/hitachi/hbsd_utils.py @@ -1,5 +1,5 @@ # Copyright (C) 2020, 2024, Hitachi, Ltd. -# Copyright (C) 2025, Hitachi Vantara +# Copyright (C) 2025, 2026, Hitachi Vantara # # 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 @@ -18,16 +18,19 @@ import enum import functools import logging as base_logging +import threading +import typing from oslo_log import log as logging from oslo_utils import timeutils from oslo_utils import units +from cinder import coordination from cinder import exception from cinder import utils as cinder_utils from cinder.volume import volume_types -VERSION = '2.7.1' +VERSION = '2.7.2' CI_WIKI_NAME = 'Hitachi_CI' PARAM_PREFIX = 'hitachi' VENDOR_NAME = 'Hitachi' @@ -1086,3 +1089,386 @@ class Config(object): super().__getattribute__(DICT)[name] = opt.type(val) else: super().__getattribute__(DICT)[name] = opt.default + + +class HostConnectorSearcher(object): + '''Searcher for host connections.''' + + def __init__(self, queryFunc: typing.Callable): + # The query function has a signature like this: + # def Query(port: str, group: int | str | None) -> + # list[str] | tuple[tuple[int, Any], list[str]] | + # list[tuple[int, Any]] + + # Query functionality if group is: + # int: do a lookup for the given group by number. + # return list of WWNs/targets found + # str: do a lookup for the given group by name. + # return tuple of group #/metadata tuple, and list of + # WWNs/targets found (tuple[tuple[int, Any], list[str]]). + # If the host group is not found, this should return None. + # and an empty list. + # other: do a lookup for all groups on the port + # return list of tuples of groups/metadata groups found + # (tuple[int, Any]) + + self._queryFunc = queryFunc + + def find(self, port: str, targetOrWwns: list[str], + groupNameHints: list[str]) -> tuple[int, typing.Any] | None: + '''Find the group for the given target or WWN.''' + + # This method finds the group for the given target or + # WWN in the cache. If it does not exist in the cache, + # it will do a search on the storage using the queryFunc. + # When performing the search, it will first use groupNameHints + # to query the host groups named there.''' + + # If we've been given groupNameHints, we'll look for those + # groups first. + groupAndMeta = None + if groupNameHints: + for groupName in groupNameHints: + if groupAndMeta is not None: # If we found our group, bail out + break + res = self._queryGroupByName(port, groupName) + if res is None: + continue + groupAndMetaTemp, targets = res + for target in targets: + # Compare target and set if found. + if target in targetOrWwns: + groupAndMeta = groupAndMetaTemp + # If we found our group, bail out. + if groupAndMeta is not None: + break + + # If we still don't have a group, we'll use our queryFunc + # to find it (if possible). + if groupAndMeta is None: + # Query the group list. + searchGroupsAndMeta = self._queryGroupsOnPort(port) + + # For each group, query the WWN(s) until we find what we're + # looking for. Cache all WWNs/groups found. + for searchGroupAndMeta in searchGroupsAndMeta: + groupTemp, metaTemp = searchGroupAndMeta + + targets = self._queryGroup(port, groupTemp) + + for target in targets: + if target in targetOrWwns: + groupAndMeta = searchGroupAndMeta + # If we found our group, bail out. + if groupAndMeta is not None: + break + + # If we found our group, bail out. + if groupAndMeta is not None: + break + + return groupAndMeta + + def _queryGroupByName(self, port: str, + group: str) -> tuple[tuple[int, typing.Any], + list[str]] | None: + return self._queryFunc(port, group) + + def _queryGroup(self, port: str, group: int) -> list[str]: + return self._queryFunc(port, group) + + def _queryGroupsOnPort(self, port: str) -> list[tuple[int, typing.Any]]: + return self._queryFunc(port, None) + + def on_reset(self): + '''Reset any caching.''' + + # This notifies that the system has changed in some way and + # is requesting a reset of any caches, etc. + pass + + def on_reset_group(self, port: str, group: int): + '''Reset any cache for the given group.''' + + # This notifies that the system has changed in some way in + # relation to the given group information and is + # requesting a reset of caches around this relationship. + pass + + +class ConnectorSearcherCache(object): + '''Cache for the host connector searcher.''' + + def __init__(self): + + # This represents the cache of ports & targets/WWNs to group numbers. + # key: str [_generate_target_key(port, target/WWN)] + # value: tuple(int, typing.Any) [group #, meta-data] + self._target_cache = dict() + # This represents the cache of port/group number to group information. + # key: str [_generate_group_key(port, group)] + # value: tuple(str | None, list[str]) [name, target/wwn list] + self._group_cache = dict() + # This represents the cache of port/group name to group number. + # key: str [_generate_group_name_key(port, groupName) + # value: int [group #] + self._group_name_cache = dict() + self._separator = '\t' + + def _generate_target_key(self, port: str, targetOrWwn: str) -> str: + return (port + self._separator + targetOrWwn) + + def _generate_group_key(self, port: str, group: int) -> str: + return (port + self._separator + str(group)) + + def _generate_group_name_key(self, port: str, groupName: str) -> str: + # Triple separators between port and group to avoid collisions. + return (port + self._separator + groupName) + + def lookup(self, port: str, + targetOrWwn: str) -> tuple[int, typing.Any] | None: + '''Find the group/meta information for the given target/WWN.''' + + key = self._generate_target_key(port, targetOrWwn) + ret = self._target_cache.get(key, None) + if ret: + LOG.debug('Found group (and meta) %(group)s for target/WWN ' + '%(target)s on port %(port)s in cache.', + {'group': ret, 'target': targetOrWwn, 'port': port}) + return ret + + def is_group_cached(self, port: str, group: int) -> bool: + '''Determine if the given group is in our cache.''' + + key = self._generate_group_key(port, group) + return self._group_cache.get(key, None) is not None + + def is_group_name_cached(self, port: str, groupName: str) -> bool: + '''Determine if the given group name is in our cache.''' + + key = self._generate_group_name_key(port, groupName) + return self._group_name_cache.get(key, None) is not None + + def cache(self, port: str, groupAndMeta: tuple[int, typing.Any], + groupName: str | None, targetsOrWwns: list[str] | None): + '''Cache the given group and its associations.''' + + targetList = list() + + # Extract our group number. + group, _ = groupAndMeta + LOG.debug("Caching information for group %(group)s.", + {'group': group}) + + # 1. Cache our targets/WWNs with the given group/meta data. + # 2. Also build our target list for our group cache. We won't use + # the given list directly to avoid a situation where it gets + # modified elsewhere. + if targetsOrWwns: + for targetOrWwn in targetsOrWwns: + key = self._generate_target_key(port, targetOrWwn) + LOG.debug("Caching target to group %(targetKey)s:%(group)s.", + {'targetKey': key, 'group': group}) + self._target_cache[key] = groupAndMeta + targetList.append(targetOrWwn) + + # Cache our group information. + key = self._generate_group_key(port, group) + self._group_cache[key] = (groupName, targetList) + + # Cache our group name information if we have any. + if groupName: + key = self._generate_group_name_key(port, groupName) + LOG.debug("Caching group name to group%(groupNameKey)s:%(group)s.", + {'groupNameKey': key, 'group': group}) + self._group_name_cache[key] = group + + def clear(self): + + # Clear the entire cache. + self._target_cache.clear() + self._group_cache.clear() + self._group_name_cache.clear() + + def clear_group(self, port: str, group: int): + + # Clear the given group from the cache. + # This will clear the group in its entirety from all 3 caches. + + groupKey = self._generate_group_key(port, group) + groupInfo = self._group_cache.get(groupKey, None) + if groupInfo: + name, targets = groupInfo + if name: + groupNameKey = self._generate_group_name_key(port, name) + del self._group_name_cache[groupNameKey] + for target in targets: + targetKey = self._generate_target_key(port, target) + del self._target_cache[targetKey] + del self._group_cache[groupKey] + + +class CachingHostConnectorSearcher(HostConnectorSearcher): + '''Caching version of the host connector searcher.''' + + def __init__(self, storage_id, queryFunc: typing.Callable): + super(CachingHostConnectorSearcher, self).__init__(queryFunc) + self._storage_id = storage_id + self._connector_cache = ConnectorSearcherCache() + self._cache_lock = threading.Lock() + + # Only allow 1 search at a time per storage/port. + # This is because it's very expensive, and when a search is ongoing + # the next caller may have already had their data cached. + # So the next caller can come in and check the cache again when they + # have access to the lock. + # Note that this will also prevent cross-node searches simultaneously. + # We may want to change that in the future, but for the time being it + # will prevent the storage API from being overwhelmed on big searches. + @coordination.synchronized( + 'target-search-{self._storage_id}-{port}') + def _locked_search(self, port: str, targetOrWwns: list[str], + groupNameHints: list[str])\ + -> tuple[int, typing.Any] | None: + '''Perform the search with a lock.''' + + # Once we have our search lock we'll do a lookup + # in the cache again as another caller may have + # been doing a simultaneous search if we waited. + groupAndMeta = None + for targetOrWwn in targetOrWwns: + groupAndMeta = self._lookup(port, targetOrWwn) + if groupAndMeta is not None: + break + + if groupAndMeta is None: + LOG.debug('Group not found in cache for port %(port)s ' + 'and target/WWNs %(targets)s. ' + 'Performing search.', + {'port': port, 'targets': targetOrWwns}) + + # If we've been given groupNameHints, we'll look for those + # groups first. + if groupNameHints: + for groupName in groupNameHints: + # If we already searched our group, skip it. + if self._is_group_name_cached(port, groupName): + LOG.debug('Skipping cached group %(group)s ' + 'on port %(port)s.', + {'group': groupName, + 'port': port}) + continue + + res = self._queryGroupByName(port, groupName) + if res is None: + continue + searchGroupAndMeta, targets = res + + # Only cache the group name if the group actually exists. + # Cache searches will eventually cache everything + # necessary. + groupAndMeta = self._find_and_cache(port, + searchGroupAndMeta, + targetOrWwns, + targets, + groupName) + + # If we still don't have a group, we'll use our queryFunc + # to find it (if possible). + if groupAndMeta is None: + # Query the group list. + searchGroupsAndMeta = self._queryGroupsOnPort(port) + + # For each group, query the WWN(s) until we find what + # we're looking for. Cache all WWNs/groups found. + for searchGroupAndMeta in searchGroupsAndMeta: + searchGroup, searchMeta = searchGroupAndMeta + if self._is_group_cached(port, searchGroup): + LOG.debug('Skipping cached group %(group)s ' + 'on port %(port)s.', + {'group': searchGroup, + 'port': port}) + continue + + targets = self._queryGroup(port, searchGroup) + groupAndMeta = self._find_and_cache(port, + searchGroupAndMeta, + targetOrWwns, targets, + None) + + # If we found our group then stop the search. + if groupAndMeta is not None: + LOG.debug('Group/meta %(group)s found for port ' + '%(port)s and target/WWN ' + '%(targets)s.', + {'group': groupAndMeta, 'port': port, + 'targets': targetOrWwns}) + break + + return groupAndMeta + + def find(self, port: str, targetOrWwns: list[str], + groupNameHints: list[str]) -> tuple[int, typing.Any] | None: + '''Find the group for the given target or WWN.''' + + # This method finds the group for the given target or + # WWN in the cache. If it does not exist in the cache, + # it will do a search on the storage using the queryFunc. + # When performing the search, it will first use groupNameHints + # to query the host groups named there.''' + groupAndMeta = None + for targetOrWwn in targetOrWwns: + groupAndMeta = self._lookup(port, targetOrWwn) + if groupAndMeta is not None: + break + if groupAndMeta is None: + groupAndMeta = self._locked_search(port, targetOrWwns, + groupNameHints) + return groupAndMeta + + def _find_and_cache(self, port: str, groupAndMeta: tuple[int, typing.Any], + searchTargets: list[str], targets: list[str], + groupName: str | None)\ + -> tuple[int, typing.Any] | None: + + with self._cache_lock: + self._connector_cache.cache(port, groupAndMeta, groupName, targets) + + foundGroup = None + if targets: + for searchTarget in searchTargets: + if searchTarget in targets: + foundGroup = groupAndMeta + break + + return foundGroup + + def _lookup(self, port: str, + targetOrWwn: str) -> tuple[int, typing.Any] | None: + with self._cache_lock: + return self._connector_cache.lookup(port, targetOrWwn) + + def _is_group_cached(self, port: str, group: int) -> bool: + with self._cache_lock: + return self._connector_cache.is_group_cached(port, group) + + def _is_group_name_cached(self, port: str, groupName: str) -> bool: + with self._cache_lock: + return self._connector_cache.is_group_name_cached(port, groupName) + + def on_reset(self): + '''Reset the entire cache.''' + + LOG.debug("Resetting entire cache.") + + with self._cache_lock: + self._connector_cache.clear() + + def on_reset_group(self, port: str, group: int): + '''Reset the cache for the given group.''' + + LOG.debug('Resetting cache for group: %(port)s-%(group)s.', + {'port': port, 'group': group}) + + with self._cache_lock: + self._connector_cache.clear_group(port, group) diff --git a/cinder/volume/drivers/hpe/xp/hpe_xp_rest.py b/cinder/volume/drivers/hpe/xp/hpe_xp_rest.py index b43a1a00319..55851345979 100644 --- a/cinder/volume/drivers/hpe/xp/hpe_xp_rest.py +++ b/cinder/volume/drivers/hpe/xp/hpe_xp_rest.py @@ -182,6 +182,15 @@ REST_VOLUME_OPTS = [ 'hpexp_host_mode_options', default=[], help='Host mode option for host group or iSCSI target.'), + cfg.BoolOpt( + 'hpexp_rest_use_object_caching', + default=True, + help='Set True to enable object caching of certain REST objects ' + 'for better performance.'), + cfg.IntOpt( + 'hpexp_rest_max_request_workers', + default=16, + help='The maximum number of workers for concurrent requests.'), ] FC_VOLUME_OPTS = [ @@ -272,6 +281,10 @@ class HPEXPRESTFC(hbsd_rest_fc.HBSDRESTFC): self.conf.hpexp_rest_tcp_keepcnt) self.conf.hitachi_host_mode_options = ( self.conf.hpexp_host_mode_options) + self.conf.hitachi_rest_use_object_caching = ( + self.conf.hpexp_rest_use_object_caching) + self.conf.hitachi_rest_max_request_workers = ( + self.conf.hpexp_rest_max_request_workers) # FC_VOLUME_OPTS self.conf.hitachi_zoning_request = self.conf.hpexp_zoning_request @@ -345,3 +358,7 @@ class HPEXPRESTISCSI(hbsd_rest_iscsi.HBSDRESTISCSI): self.conf.hpexp_rest_tcp_keepcnt) self.conf.hitachi_host_mode_options = ( self.conf.hpexp_host_mode_options) + self.conf.hitachi_rest_use_object_caching = ( + self.conf.hpexp_rest_use_object_caching) + self.conf.hitachi_rest_max_request_workers = ( + self.conf.hpexp_rest_max_request_workers) diff --git a/cinder/volume/drivers/nec/v/nec_v_rest.py b/cinder/volume/drivers/nec/v/nec_v_rest.py index cf1ad52eb87..756e9940f0b 100644 --- a/cinder/volume/drivers/nec/v/nec_v_rest.py +++ b/cinder/volume/drivers/nec/v/nec_v_rest.py @@ -258,6 +258,10 @@ def update_conf(conf): conf.nec_v_rest_tcp_keepcnt) conf.hitachi_host_mode_options = ( conf.nec_v_host_mode_options) + conf.hitachi_rest_use_object_caching = ( + conf.nec_v_rest_use_object_caching) + conf.hitachi_rest_max_request_workers = ( + conf.nec_v_rest_max_request_workers) return conf