Merge "[Hitachi] Bug fix: Introduce Host Group and target/WWN caching and batching to address severe performance issues (especially when using custom Host Groups, or creating several VMs at once)."
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user