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:
Zuul
2026-03-14 02:25:29 +00:00
committed by Gerrit Code Review
20 changed files with 1227 additions and 148 deletions
@@ -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']
+1
View File
@@ -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.
"""
+86 -17
View File
@@ -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."""
+13 -5
View File
@@ -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):
+68 -60
View File
@@ -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(
+388 -2
View File
@@ -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