From 36e00875772a80cda6ece752a202333bd911e8e6 Mon Sep 17 00:00:00 2001 From: Kazumasa Nomura Date: Tue, 7 Sep 2021 10:05:35 +0000 Subject: [PATCH] Hitachi: Add generic volume groups This patch adds consistency group capability as generic volume groups for the Hitachi driver. DocImpact Implements: blueprint hitachi-vsp-add-consistency-groups Change-Id: I101d6899c8e7d4911c64cded2c10da68f5bceed2 --- .../unit/volume/drivers/hitachi/__init__.py | 0 .../hitachi/test_hitachi_hbsd_rest_fc.py | 190 +++++++++++- .../hitachi/test_hitachi_hbsd_rest_iscsi.py | 189 +++++++++++- cinder/volume/drivers/hitachi/__init__.py | 0 cinder/volume/drivers/hitachi/hbsd_common.py | 25 +- cinder/volume/drivers/hitachi/hbsd_fc.py | 36 +++ cinder/volume/drivers/hitachi/hbsd_iscsi.py | 37 +++ cinder/volume/drivers/hitachi/hbsd_rest.py | 279 ++++++++++++++++++ .../volume/drivers/hitachi/hbsd_rest_api.py | 8 + cinder/volume/drivers/hitachi/hbsd_utils.py | 49 +++ .../drivers/hitachi-vsp-driver.rst | 2 + doc/source/reference/support-matrix.ini | 2 +- ...eneric-volume-groups-434a27b290d51bf3.yaml | 4 + 13 files changed, 805 insertions(+), 16 deletions(-) create mode 100644 cinder/tests/unit/volume/drivers/hitachi/__init__.py create mode 100644 cinder/volume/drivers/hitachi/__init__.py create mode 100644 releasenotes/notes/hitachi-generic-volume-groups-434a27b290d51bf3.yaml diff --git a/cinder/tests/unit/volume/drivers/hitachi/__init__.py b/cinder/tests/unit/volume/drivers/hitachi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_fc.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_fc.py index 006df106838..1e1e76dfe22 100644 --- a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_fc.py +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_fc.py @@ -24,11 +24,13 @@ from requests import models from cinder import context as cinder_context from cinder import db from cinder.db.sqlalchemy import api as sqlalchemy_api +from cinder.objects import group_snapshot as obj_group_snap from cinder.objects import snapshot as obj_snap +from cinder.tests.unit import fake_group +from cinder.tests.unit import fake_group_snapshot from cinder.tests.unit import fake_snapshot from cinder.tests.unit import fake_volume from cinder.tests.unit import test -from cinder import utils from cinder.volume import configuration as conf from cinder.volume import driver from cinder.volume.drivers.hitachi import hbsd_common @@ -37,6 +39,7 @@ from cinder.volume.drivers.hitachi import hbsd_rest from cinder.volume.drivers.hitachi import hbsd_rest_api from cinder.volume.drivers.hitachi import hbsd_utils from cinder.volume import volume_types +from cinder.volume import volume_utils from cinder.zonemanager import utils as fczm_utils # Configuration parameter values @@ -73,11 +76,14 @@ DEFAULT_CONNECTOR = { CTXT = cinder_context.get_admin_context() TEST_VOLUME = [] -for i in range(3): +for i in range(4): volume = {} volume['id'] = '00000000-0000-0000-0000-{0:012d}'.format(i) volume['name'] = 'test-volume{0:d}'.format(i) - volume['provider_location'] = '{0:d}'.format(i) + if i == 3: + volume['provider_location'] = None + else: + volume['provider_location'] = '{0:d}'.format(i) volume['size'] = 128 if i == 2: volume['status'] = 'in-use' @@ -107,6 +113,23 @@ snapshot = obj_snap.Snapshot._from_db_object( fake_snapshot.fake_db_snapshot(**snapshot)) TEST_SNAPSHOT.append(snapshot) +TEST_GROUP = [] +for i in range(2): + group = {} + group['id'] = '20000000-0000-0000-0000-{0:012d}'.format(i) + group['status'] = 'available' + group = fake_group.fake_group_obj(CTXT, **group) + TEST_GROUP.append(group) + +TEST_GROUP_SNAP = [] +group_snapshot = {} +group_snapshot['id'] = '30000000-0000-0000-0000-{0:012d}'.format(0) +group_snapshot['status'] = 'available' +group_snapshot = obj_group_snap.GroupSnapshot._from_db_object( + CTXT, obj_group_snap.GroupSnapshot(), + fake_group_snapshot.fake_db_group_snapshot(**group_snapshot)) +TEST_GROUP_SNAP.append(group_snapshot) + # Dummy response for REST API POST_SESSIONS_RESULT = { "token": "b74777a3-f9f0-4ea8-bd8f-09847fac48d3", @@ -205,6 +228,18 @@ GET_SNAPSHOTS_RESULT = { ], } +GET_SNAPSHOTS_RESULT_PAIR = { + "data": [ + { + "primaryOrSecondary": "S-VOL", + "status": "PAIR", + "pvolLdevId": 0, + "muNumber": 1, + "svolLdevId": 1, + }, + ], +} + GET_SNAPSHOTS_RESULT_BUSY = { "data": [ { @@ -403,6 +438,7 @@ class HBSDRESTFCDriverTest(test.TestCase): 'rest_server_ip_port'] self.configuration.hitachi_rest_tcp_keepalive = True self.configuration.hitachi_discard_zero_page = True + self.configuration.hitachi_rest_number = "0" self.configuration.hitachi_zoning_request = False @@ -434,7 +470,7 @@ class HBSDRESTFCDriverTest(test.TestCase): @mock.patch.object(requests.Session, "request") @mock.patch.object( - utils, 'brick_get_connector_properties', + volume_utils, 'brick_get_connector_properties', side_effect=_brick_get_connector_properties) def _setup_driver( self, brick_get_connector_properties=None, request=None): @@ -462,7 +498,7 @@ class HBSDRESTFCDriverTest(test.TestCase): # API test cases @mock.patch.object(requests.Session, "request") @mock.patch.object( - utils, 'brick_get_connector_properties', + volume_utils, 'brick_get_connector_properties', side_effect=_brick_get_connector_properties) def test_do_setup(self, brick_get_connector_properties, request): drv = hbsd_fc.HBSDFCDriver( @@ -483,7 +519,7 @@ class HBSDRESTFCDriverTest(test.TestCase): @mock.patch.object(requests.Session, "request") @mock.patch.object( - utils, 'brick_get_connector_properties', + volume_utils, 'brick_get_connector_properties', side_effect=_brick_get_connector_properties) def test_do_setup_create_hg(self, brick_get_connector_properties, request): """Normal case: The host group not exists.""" @@ -510,7 +546,7 @@ class HBSDRESTFCDriverTest(test.TestCase): @mock.patch.object(requests.Session, "request") @mock.patch.object( - utils, 'brick_get_connector_properties', + volume_utils, 'brick_get_connector_properties', side_effect=_brick_get_connector_properties) def test_do_setup_pool_name(self, brick_get_connector_properties, request): """Normal case: Specify a pool name instead of pool id""" @@ -896,3 +932,143 @@ class HBSDRESTFCDriverTest(test.TestCase): req = models.Response() ret = session.__call__(req) self.assertEqual('Session token', ret.headers['Authorization']) + + def test_create_group(self): + ret = self.driver.create_group(self.ctxt, TEST_GROUP[0]) + self.assertIsNone(ret) + + @mock.patch.object(requests.Session, "request") + def test_delete_group(self, request): + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)] + ret = self.driver.delete_group( + self.ctxt, TEST_GROUP[0], [TEST_VOLUME[0]]) + self.assertEqual(4, request.call_count) + actual = ( + {'status': TEST_GROUP[0]['status']}, + [{'id': TEST_VOLUME[0]['id'], 'status': 'deleted'}] + ) + self.assertTupleEqual(actual, ret) + + @mock.patch.object(requests.Session, "request") + def test_create_group_from_src_volume(self, request): + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)] + ret = self.driver.create_group_from_src( + self.ctxt, TEST_GROUP[1], [TEST_VOLUME[1]], + source_group=TEST_GROUP[0], source_vols=[TEST_VOLUME[0]] + ) + self.assertEqual(5, request.call_count) + actual = ( + None, [{'id': TEST_VOLUME[1]['id'], 'provider_location': '1'}]) + self.assertTupleEqual(actual, ret) + + @mock.patch.object(requests.Session, "request") + def test_create_group_from_src_snapshot(self, request): + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)] + ret = self.driver.create_group_from_src( + self.ctxt, TEST_GROUP[0], [TEST_VOLUME[0]], + group_snapshot=TEST_GROUP_SNAP[0], snapshots=[TEST_SNAPSHOT[0]] + ) + self.assertEqual(5, request.call_count) + actual = ( + None, [{'id': TEST_VOLUME[0]['id'], 'provider_location': '1'}]) + self.assertTupleEqual(actual, ret) + + def test_create_group_from_src_volume_error(self): + self.assertRaises( + hbsd_utils.HBSDError, self.driver.create_group_from_src, + self.ctxt, TEST_GROUP[1], [TEST_VOLUME[1]], + source_group=TEST_GROUP[0], source_vols=[TEST_VOLUME[3]] + ) + + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type') + def test_update_group(self, is_group_a_cg_snapshot_type): + is_group_a_cg_snapshot_type.return_value = False + ret = self.driver.update_group( + self.ctxt, TEST_GROUP[0], add_volumes=[TEST_VOLUME[0]]) + self.assertTupleEqual((None, None, None), ret) + + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type') + def test_update_group_error(self, is_group_a_cg_snapshot_type): + is_group_a_cg_snapshot_type.return_value = True + self.assertRaises( + hbsd_utils.HBSDError, self.driver.update_group, + self.ctxt, TEST_GROUP[0], add_volumes=[TEST_VOLUME[3]], + remove_volumes=[TEST_VOLUME[0]] + ) + + @mock.patch.object(requests.Session, "request") + @mock.patch.object(sqlalchemy_api, 'volume_get', side_effect=_volume_get) + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type') + def test_create_group_snapshot_non_cg( + self, is_group_a_cg_snapshot_type, volume_get, request): + is_group_a_cg_snapshot_type.return_value = False + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT)] + ret = self.driver.create_group_snapshot( + self.ctxt, TEST_GROUP_SNAP[0], [TEST_SNAPSHOT[0]] + ) + self.assertEqual(4, request.call_count) + actual = ( + {'status': 'available'}, + [{'id': TEST_SNAPSHOT[0]['id'], + 'provider_location': '1', + 'status': 'available'}] + ) + self.assertTupleEqual(actual, ret) + + @mock.patch.object(requests.Session, "request") + @mock.patch.object(sqlalchemy_api, 'volume_get', side_effect=_volume_get) + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type') + def test_create_group_snapshot_cg( + self, is_group_a_cg_snapshot_type, volume_get, request): + is_group_a_cg_snapshot_type.return_value = True + request.side_effect = [FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT_PAIR), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT)] + ret = self.driver.create_group_snapshot( + self.ctxt, TEST_GROUP_SNAP[0], [TEST_SNAPSHOT[0]] + ) + self.assertEqual(5, request.call_count) + actual = ( + None, + [{'id': TEST_SNAPSHOT[0]['id'], + 'provider_location': '1', + 'status': 'available'}] + ) + self.assertTupleEqual(actual, ret) + + @mock.patch.object(requests.Session, "request") + def test_delete_group_snapshot(self, request): + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT_PAIR), + FakeResponse(200, NOTFOUND_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)] + ret = self.driver.delete_group_snapshot( + self.ctxt, TEST_GROUP_SNAP[0], [TEST_SNAPSHOT[0]]) + self.assertEqual(10, request.call_count) + actual = ( + {'status': TEST_GROUP_SNAP[0]['status']}, + [{'id': TEST_SNAPSHOT[0]['id'], 'status': 'deleted'}] + ) + self.assertTupleEqual(actual, ret) diff --git a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_iscsi.py b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_iscsi.py index 9e2e30489c2..311f21a3e56 100644 --- a/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_iscsi.py +++ b/cinder/tests/unit/volume/drivers/hitachi/test_hitachi_hbsd_rest_iscsi.py @@ -22,17 +22,21 @@ import requests from cinder import context as cinder_context from cinder import db from cinder.db.sqlalchemy import api as sqlalchemy_api +from cinder.objects import group_snapshot as obj_group_snap from cinder.objects import snapshot as obj_snap +from cinder.tests.unit import fake_group +from cinder.tests.unit import fake_group_snapshot from cinder.tests.unit import fake_snapshot from cinder.tests.unit import fake_volume from cinder.tests.unit import test -from cinder import utils from cinder.volume import configuration as conf from cinder.volume import driver from cinder.volume.drivers.hitachi import hbsd_common from cinder.volume.drivers.hitachi import hbsd_iscsi from cinder.volume.drivers.hitachi import hbsd_rest +from cinder.volume.drivers.hitachi import hbsd_utils from cinder.volume import volume_types +from cinder.volume import volume_utils # Configuration parameter values CONFIG_MAP = { @@ -63,11 +67,14 @@ DEFAULT_CONNECTOR = { CTXT = cinder_context.get_admin_context() TEST_VOLUME = [] -for i in range(3): +for i in range(4): volume = {} volume['id'] = '00000000-0000-0000-0000-{0:012d}'.format(i) volume['name'] = 'test-volume{0:d}'.format(i) - volume['provider_location'] = '{0:d}'.format(i) + if i == 3: + volume['provider_location'] = None + else: + volume['provider_location'] = '{0:d}'.format(i) volume['size'] = 128 if i == 2: volume['status'] = 'in-use' @@ -97,6 +104,23 @@ snapshot = obj_snap.Snapshot._from_db_object( fake_snapshot.fake_db_snapshot(**snapshot)) TEST_SNAPSHOT.append(snapshot) +TEST_GROUP = [] +for i in range(2): + group = {} + group['id'] = '20000000-0000-0000-0000-{0:012d}'.format(i) + group['status'] = 'available' + group = fake_group.fake_group_obj(CTXT, **group) + TEST_GROUP.append(group) + +TEST_GROUP_SNAP = [] +group_snapshot = {} +group_snapshot['id'] = '30000000-0000-0000-0000-{0:012d}'.format(0) +group_snapshot['status'] = 'available' +group_snapshot = obj_group_snap.GroupSnapshot._from_db_object( + CTXT, obj_group_snap.GroupSnapshot(), + fake_group_snapshot.fake_db_group_snapshot(**group_snapshot)) +TEST_GROUP_SNAP.append(group_snapshot) + # Dummy response for REST API POST_SESSIONS_RESULT = { "token": "b74777a3-f9f0-4ea8-bd8f-09847fac48d3", @@ -205,6 +229,18 @@ GET_SNAPSHOTS_RESULT = { ], } +GET_SNAPSHOTS_RESULT_PAIR = { + "data": [ + { + "primaryOrSecondary": "S-VOL", + "status": "PAIR", + "pvolLdevId": 0, + "muNumber": 1, + "svolLdevId": 1, + }, + ], +} + GET_LDEVS_RESULT = { "data": [ { @@ -301,6 +337,7 @@ class HBSDRESTISCSIDriverTest(test.TestCase): 'rest_server_ip_port'] self.configuration.hitachi_rest_tcp_keepalive = True self.configuration.hitachi_discard_zero_page = True + self.configuration.hitachi_rest_number = "0" self.configuration.use_chap_auth = True self.configuration.chap_username = CONFIG_MAP['auth_user'] @@ -330,7 +367,7 @@ class HBSDRESTISCSIDriverTest(test.TestCase): @mock.patch.object(requests.Session, "request") @mock.patch.object( - utils, 'brick_get_connector_properties', + volume_utils, 'brick_get_connector_properties', side_effect=_brick_get_connector_properties) def _setup_driver( self, brick_get_connector_properties=None, request=None): @@ -360,7 +397,7 @@ class HBSDRESTISCSIDriverTest(test.TestCase): # API test cases @mock.patch.object(requests.Session, "request") @mock.patch.object( - utils, 'brick_get_connector_properties', + volume_utils, 'brick_get_connector_properties', side_effect=_brick_get_connector_properties) def test_do_setup(self, brick_get_connector_properties, request): drv = hbsd_iscsi.HBSDISCSIDriver( @@ -386,7 +423,7 @@ class HBSDRESTISCSIDriverTest(test.TestCase): @mock.patch.object(requests.Session, "request") @mock.patch.object( - utils, 'brick_get_connector_properties', + volume_utils, 'brick_get_connector_properties', side_effect=_brick_get_connector_properties) def test_do_setup_create_hg(self, brick_get_connector_properties, request): """Normal case: The host group not exists.""" @@ -700,3 +737,143 @@ class HBSDRESTISCSIDriverTest(test.TestCase): self.driver.revert_to_snapshot( self.ctxt, TEST_VOLUME[0], TEST_SNAPSHOT[0]) self.assertEqual(5, request.call_count) + + def test_create_group(self): + ret = self.driver.create_group(self.ctxt, TEST_GROUP[0]) + self.assertIsNone(ret) + + @mock.patch.object(requests.Session, "request") + def test_delete_group(self, request): + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)] + ret = self.driver.delete_group( + self.ctxt, TEST_GROUP[0], [TEST_VOLUME[0]]) + self.assertEqual(4, request.call_count) + actual = ( + {'status': TEST_GROUP[0]['status']}, + [{'id': TEST_VOLUME[0]['id'], 'status': 'deleted'}] + ) + self.assertTupleEqual(actual, ret) + + @mock.patch.object(requests.Session, "request") + def test_create_group_from_src_volume(self, request): + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)] + ret = self.driver.create_group_from_src( + self.ctxt, TEST_GROUP[1], [TEST_VOLUME[1]], + source_group=TEST_GROUP[0], source_vols=[TEST_VOLUME[0]] + ) + self.assertEqual(5, request.call_count) + actual = ( + None, [{'id': TEST_VOLUME[1]['id'], 'provider_location': '1'}]) + self.assertTupleEqual(actual, ret) + + @mock.patch.object(requests.Session, "request") + def test_create_group_from_src_snapshot(self, request): + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)] + ret = self.driver.create_group_from_src( + self.ctxt, TEST_GROUP[0], [TEST_VOLUME[0]], + group_snapshot=TEST_GROUP_SNAP[0], snapshots=[TEST_SNAPSHOT[0]] + ) + self.assertEqual(5, request.call_count) + actual = ( + None, [{'id': TEST_VOLUME[0]['id'], 'provider_location': '1'}]) + self.assertTupleEqual(actual, ret) + + def test_create_group_from_src_volume_error(self): + self.assertRaises( + hbsd_utils.HBSDError, self.driver.create_group_from_src, + self.ctxt, TEST_GROUP[1], [TEST_VOLUME[1]], + source_group=TEST_GROUP[0], source_vols=[TEST_VOLUME[3]] + ) + + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type') + def test_update_group(self, is_group_a_cg_snapshot_type): + is_group_a_cg_snapshot_type.return_value = False + ret = self.driver.update_group( + self.ctxt, TEST_GROUP[0], add_volumes=[TEST_VOLUME[0]]) + self.assertTupleEqual((None, None, None), ret) + + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type') + def test_update_group_error(self, is_group_a_cg_snapshot_type): + is_group_a_cg_snapshot_type.return_value = True + self.assertRaises( + hbsd_utils.HBSDError, self.driver.update_group, + self.ctxt, TEST_GROUP[0], add_volumes=[TEST_VOLUME[3]], + remove_volumes=[TEST_VOLUME[0]] + ) + + @mock.patch.object(requests.Session, "request") + @mock.patch.object(sqlalchemy_api, 'volume_get', side_effect=_volume_get) + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type') + def test_create_group_snapshot_non_cg( + self, is_group_a_cg_snapshot_type, volume_get, request): + is_group_a_cg_snapshot_type.return_value = False + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT)] + ret = self.driver.create_group_snapshot( + self.ctxt, TEST_GROUP_SNAP[0], [TEST_SNAPSHOT[0]] + ) + self.assertEqual(4, request.call_count) + actual = ( + {'status': 'available'}, + [{'id': TEST_SNAPSHOT[0]['id'], + 'provider_location': '1', + 'status': 'available'}] + ) + self.assertTupleEqual(actual, ret) + + @mock.patch.object(requests.Session, "request") + @mock.patch.object(sqlalchemy_api, 'volume_get', side_effect=_volume_get) + @mock.patch.object(volume_utils, 'is_group_a_cg_snapshot_type') + def test_create_group_snapshot_cg( + self, is_group_a_cg_snapshot_type, volume_get, request): + is_group_a_cg_snapshot_type.return_value = True + request.side_effect = [FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT_PAIR), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT)] + ret = self.driver.create_group_snapshot( + self.ctxt, TEST_GROUP_SNAP[0], [TEST_SNAPSHOT[0]] + ) + self.assertEqual(5, request.call_count) + actual = ( + None, + [{'id': TEST_SNAPSHOT[0]['id'], + 'provider_location': '1', + 'status': 'available'}] + ) + self.assertTupleEqual(actual, ret) + + @mock.patch.object(requests.Session, "request") + def test_delete_group_snapshot(self, request): + request.side_effect = [FakeResponse(200, GET_LDEV_RESULT_PAIR), + FakeResponse(200, NOTFOUND_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT), + FakeResponse(200, GET_SNAPSHOTS_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(200, GET_LDEV_RESULT), + FakeResponse(202, COMPLETED_SUCCEEDED_RESULT)] + ret = self.driver.delete_group_snapshot( + self.ctxt, TEST_GROUP_SNAP[0], [TEST_SNAPSHOT[0]]) + self.assertEqual(10, request.call_count) + actual = ( + {'status': TEST_GROUP_SNAP[0]['status']}, + [{'id': TEST_SNAPSHOT[0]['id'], 'status': 'deleted'}] + ) + self.assertTupleEqual(actual, ret) diff --git a/cinder/volume/drivers/hitachi/__init__.py b/cinder/volume/drivers/hitachi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cinder/volume/drivers/hitachi/hbsd_common.py b/cinder/volume/drivers/hitachi/hbsd_common.py index 79596032c59..c02d7d725a2 100644 --- a/cinder/volume/drivers/hitachi/hbsd_common.py +++ b/cinder/volume/drivers/hitachi/hbsd_common.py @@ -26,7 +26,7 @@ from cinder.volume import configuration from cinder.volume.drivers.hitachi import hbsd_utils as utils from cinder.volume import volume_utils -VERSION = '2.0.0' +VERSION = '2.1.0' _STR_VOLUME = 'volume' _STR_SNAPSHOT = 'snapshot' @@ -318,7 +318,9 @@ class HBSDCommon(): reserved_percentage=self.conf.safe_get('reserved_percentage'), QoS_support=False, thick_provisioning_support=False, - multiattach=True + multiattach=True, + consistencygroup_support=True, + consistent_group_snapshot_enabled=True )) try: (total_capacity, free_capacity, @@ -779,3 +781,22 @@ class HBSDCommon(): self.restore_ldev(pvol, svol) else: raise NotImplementedError() + + def create_group(self): + raise NotImplementedError() + + def delete_group(self, group, volumes): + raise NotImplementedError() + + def create_group_from_src( + self, context, group, volumes, snapshots=None, source_vols=None): + raise NotImplementedError() + + def update_group(self, group, add_volumes=None): + raise NotImplementedError() + + def create_group_snapshot(self, context, group_snapshot, snapshots): + raise NotImplementedError() + + def delete_group_snapshot(self, group_snapshot, snapshots): + raise NotImplementedError() diff --git a/cinder/volume/drivers/hitachi/hbsd_fc.py b/cinder/volume/drivers/hitachi/hbsd_fc.py index f5873b737c5..8804bbf0588 100644 --- a/cinder/volume/drivers/hitachi/hbsd_fc.py +++ b/cinder/volume/drivers/hitachi/hbsd_fc.py @@ -15,6 +15,7 @@ """Fibre channel module for Hitachi HBSD Driver.""" from oslo_config import cfg +from oslo_utils import excutils from cinder import interface from cinder.volume import configuration @@ -64,6 +65,7 @@ class HBSDFCDriver(driver.FibreChannelDriver): 1.1.0 - Add manage_existing/manage_existing_get_size/unmanage methods 2.0.0 - Major redesign of the driver. This version requires the REST API for communication with the storage backend. + 2.1.0 - Add Cinder generic volume groups. """ @@ -228,3 +230,37 @@ class HBSDFCDriver(driver.FibreChannelDriver): def revert_to_snapshot(self, context, volume, snapshot): """Rollback the specified snapshot""" return self.common.revert_to_snapshot(volume, snapshot) + + @volume_utils.trace + def create_group(self, context, group): + return self.common.create_group() + + @volume_utils.trace + def delete_group(self, context, group, volumes): + return self.common.delete_group(group, volumes) + + @volume_utils.trace + def create_group_from_src( + self, context, group, volumes, group_snapshot=None, snapshots=None, + source_group=None, source_vols=None): + return self.common.create_group_from_src( + context, group, volumes, snapshots, source_vols) + + @volume_utils.trace + def update_group( + self, context, group, add_volumes=None, remove_volumes=None): + try: + return self.common.update_group(group, add_volumes) + except Exception: + with excutils.save_and_reraise_exception(): + for remove_volume in remove_volumes: + utils.cleanup_cg_in_volume(remove_volume) + + @volume_utils.trace + def create_group_snapshot(self, context, group_snapshot, snapshots): + return self.common.create_group_snapshot( + context, group_snapshot, snapshots) + + @volume_utils.trace + def delete_group_snapshot(self, context, group_snapshot, snapshots): + return self.common.delete_group_snapshot(group_snapshot, snapshots) diff --git a/cinder/volume/drivers/hitachi/hbsd_iscsi.py b/cinder/volume/drivers/hitachi/hbsd_iscsi.py index aaddc0b96fe..a63e401773e 100644 --- a/cinder/volume/drivers/hitachi/hbsd_iscsi.py +++ b/cinder/volume/drivers/hitachi/hbsd_iscsi.py @@ -14,6 +14,8 @@ # """iSCSI module for Hitachi HBSD Driver.""" +from oslo_utils import excutils + from cinder import interface from cinder.volume import driver from cinder.volume.drivers.hitachi import hbsd_common as common @@ -49,6 +51,7 @@ class HBSDISCSIDriver(driver.ISCSIDriver): 1.1.0 - Add manage_existing/manage_existing_get_size/unmanage methods 2.0.0 - Major redesign of the driver. This version requires the REST API for communication with the storage backend. + 2.1.0 - Add Cinder generic volume groups. """ @@ -212,3 +215,37 @@ class HBSDISCSIDriver(driver.ISCSIDriver): def revert_to_snapshot(self, context, volume, snapshot): """Rollback the specified snapshot""" return self.common.revert_to_snapshot(volume, snapshot) + + @volume_utils.trace + def create_group(self, context, group): + return self.common.create_group() + + @volume_utils.trace + def delete_group(self, context, group, volumes): + return self.common.delete_group(group, volumes) + + @volume_utils.trace + def create_group_from_src( + self, context, group, volumes, group_snapshot=None, snapshots=None, + source_group=None, source_vols=None): + return self.common.create_group_from_src( + context, group, volumes, snapshots, source_vols) + + @volume_utils.trace + def update_group( + self, context, group, add_volumes=None, remove_volumes=None): + try: + return self.common.update_group(group, add_volumes) + except Exception: + with excutils.save_and_reraise_exception(): + for remove_volume in remove_volumes: + utils.cleanup_cg_in_volume(remove_volume) + + @volume_utils.trace + def create_group_snapshot(self, context, group_snapshot, snapshots): + return self.common.create_group_snapshot( + context, group_snapshot, snapshots) + + @volume_utils.trace + def delete_group_snapshot(self, context, group_snapshot, snapshots): + return self.common.delete_group_snapshot(group_snapshot, snapshots) diff --git a/cinder/volume/drivers/hitachi/hbsd_rest.py b/cinder/volume/drivers/hitachi/hbsd_rest.py index 21368c9dcd6..98c03f8f6a8 100644 --- a/cinder/volume/drivers/hitachi/hbsd_rest.py +++ b/cinder/volume/drivers/hitachi/hbsd_rest.py @@ -24,11 +24,13 @@ from oslo_utils import timeutils from oslo_utils import units from cinder import exception +from cinder.objects import fields from cinder.volume import configuration from cinder.volume.drivers.hitachi import hbsd_common as common from cinder.volume.drivers.hitachi import hbsd_rest_api as rest_api from cinder.volume.drivers.hitachi import hbsd_utils as utils from cinder.volume.drivers.san import san +from cinder.volume import volume_utils _LU_PATH_DEFINED = ('B958', '015A') NORMAL_STS = 'NML' @@ -86,6 +88,9 @@ EX_ENLDEV = 'EX_ENLDEV' EX_INVARG = 'EX_INVARG' _INVALID_RANGE = [EX_ENLDEV, EX_INVARG] +_MAX_COPY_GROUP_NAME = 29 +_MAX_CTG_COUNT_EXCEEDED_ADD_SNAPSHOT = ('2E10', '2302') +_MAX_PAIR_COUNT_IN_CTG_EXCEEDED_ADD_SNAPSHOT = ('2E13', '9900') REST_VOLUME_OPTS = [ cfg.BoolOpt( @@ -789,3 +794,277 @@ class HBSDREST(common.HBSDCommon): return False return (result[0]['primaryOrSecondary'] == "S-VOL" and int(result[0]['pvolLdevId']) == pvol) + + def create_group(self): + return None + + def _delete_group(self, group, objs, is_snapshot): + model_update = {'status': group.status} + objs_model_update = [] + events = [] + + def _delete_group_obj(group, obj, is_snapshot): + obj_update = {'id': obj.id} + try: + if is_snapshot: + self.delete_snapshot(obj) + else: + self.delete_volume(obj) + obj_update['status'] = 'deleted' + except (utils.HBSDError, exception.VolumeIsBusy, + exception.SnapshotIsBusy) as exc: + obj_update['status'] = 'available' if isinstance( + exc, (exception.VolumeIsBusy, + exception.SnapshotIsBusy)) else 'error' + utils.output_log( + MSG.GROUP_OBJECT_DELETE_FAILED, + obj='snapshot' if is_snapshot else 'volume', + group='group snapshot' if is_snapshot else 'group', + group_id=group.id, obj_id=obj.id, ldev=utils.get_ldev(obj), + reason=exc.msg) + raise loopingcall.LoopingCallDone(obj_update) + + for obj in objs: + loop = loopingcall.FixedIntervalLoopingCall( + _delete_group_obj, group, obj, is_snapshot) + event = loop.start(interval=0) + events.append(event) + for e in events: + obj_update = e.wait() + if obj_update['status'] != 'deleted': + model_update['status'] = 'error' + objs_model_update.append(obj_update) + return model_update, objs_model_update + + def delete_group(self, group, volumes): + return self._delete_group(group, volumes, False) + + def delete_group_snapshot(self, group_snapshot, snapshots): + return self._delete_group(group_snapshot, snapshots, True) + + def create_group_from_src( + self, context, group, volumes, snapshots=None, source_vols=None): + volumes_model_update = [] + new_ldevs = [] + events = [] + + def _create_group_volume_from_src(context, volume, src, from_snapshot): + volume_model_update = {'id': volume.id} + try: + ldev = utils.get_ldev(src) + if ldev is None: + msg = utils.output_log( + MSG.INVALID_LDEV_FOR_VOLUME_COPY, + type='snapshot' if from_snapshot else 'volume', + id=src.id) + raise utils.HBSDError(msg) + volume_model_update.update( + self.create_volume_from_snapshot(volume, src) if + from_snapshot else self.create_cloned_volume(volume, + src)) + except Exception as exc: + volume_model_update['msg'] = utils.get_exception_msg(exc) + raise loopingcall.LoopingCallDone(volume_model_update) + + try: + from_snapshot = True if snapshots else False + for volume, src in zip(volumes, + snapshots if snapshots else source_vols): + loop = loopingcall.FixedIntervalLoopingCall( + _create_group_volume_from_src, context, volume, src, + from_snapshot) + event = loop.start(interval=0) + events.append(event) + is_success = True + for e in events: + volume_model_update = e.wait() + if 'msg' in volume_model_update: + is_success = False + msg = volume_model_update['msg'] + else: + volumes_model_update.append(volume_model_update) + ldev = utils.get_ldev(volume_model_update) + if ldev is not None: + new_ldevs.append(ldev) + if not is_success: + raise utils.HBSDError(msg) + except Exception: + with excutils.save_and_reraise_exception(): + for new_ldev in new_ldevs: + try: + self.delete_ldev(new_ldev) + except utils.HBSDError: + utils.output_log(MSG.DELETE_LDEV_FAILED, ldev=new_ldev) + return None, volumes_model_update + + def update_group(self, group, add_volumes=None): + if add_volumes and volume_utils.is_group_a_cg_snapshot_type(group): + for volume in add_volumes: + ldev = utils.get_ldev(volume) + if ldev is None: + msg = utils.output_log(MSG.LDEV_NOT_EXIST_FOR_ADD_GROUP, + volume_id=volume.id, + group='consistency group', + group_id=group.id) + raise utils.HBSDError(msg) + return None, None, None + + def _create_non_cgsnapshot(self, group_snapshot, snapshots): + model_update = {'status': fields.GroupSnapshotStatus.AVAILABLE} + snapshots_model_update = [] + events = [] + + def _create_non_cgsnapshot_snapshot(group_snapshot, snapshot): + snapshot_model_update = {'id': snapshot.id} + try: + snapshot_model_update.update(self.create_snapshot(snapshot)) + snapshot_model_update['status'] = ( + fields.SnapshotStatus.AVAILABLE) + except Exception: + snapshot_model_update['status'] = fields.SnapshotStatus.ERROR + utils.output_log( + MSG.GROUP_SNAPSHOT_CREATE_FAILED, + group=group_snapshot.group_id, + group_snapshot=group_snapshot.id, + group_type=group_snapshot.group_type_id, + volume=snapshot.volume_id, snapshot=snapshot.id) + raise loopingcall.LoopingCallDone(snapshot_model_update) + + for snapshot in snapshots: + loop = loopingcall.FixedIntervalLoopingCall( + _create_non_cgsnapshot_snapshot, group_snapshot, snapshot) + event = loop.start(interval=0) + events.append(event) + for e in events: + snapshot_model_update = e.wait() + if (snapshot_model_update['status'] == + fields.SnapshotStatus.ERROR): + model_update['status'] = fields.GroupSnapshotStatus.ERROR + snapshots_model_update.append(snapshot_model_update) + return model_update, snapshots_model_update + + def _create_ctg_snapshot_group_name(self, ldev): + now = timeutils.utcnow() + strnow = now.strftime("%y%m%d%H%M%S%f") + ctg_name = '%(prefix)sC%(ldev)s%(time)s' % { + 'prefix': utils.DRIVER_PREFIX, + 'ldev': "{0:06X}".format(ldev), + 'time': strnow[:len(strnow) - 3], + } + return ctg_name[:_MAX_COPY_GROUP_NAME] + + def _delete_pairs_from_storage(self, pairs): + for pair in pairs: + try: + self._delete_pair_from_storage(pair['pvol'], pair['svol']) + except utils.HBSDError: + utils.output_log(MSG.DELETE_PAIR_FAILED, pvol=pair['pvol'], + svol=pair['svol']) + + def _create_ctg_snap_pair(self, pairs): + snapshotgroup_name = self._create_ctg_snapshot_group_name( + pairs[0]['pvol']) + try: + for pair in pairs: + try: + body = {"snapshotGroupName": snapshotgroup_name, + "snapshotPoolId": + self.storage_info['snap_pool_id'], + "pvolLdevId": pair['pvol'], + "svolLdevId": pair['svol'], + "isConsistencyGroup": True, + "canCascade": True, + "isDataReductionForceCopy": True} + self.client.add_snapshot(body) + except utils.HBSDError as ex: + if ((utils.safe_get_err_code(ex.kwargs.get('errobj')) == + _MAX_CTG_COUNT_EXCEEDED_ADD_SNAPSHOT) or + (utils.safe_get_err_code(ex.kwargs.get('errobj')) == + _MAX_PAIR_COUNT_IN_CTG_EXCEEDED_ADD_SNAPSHOT)): + msg = utils.output_log(MSG.FAILED_CREATE_CTG_SNAPSHOT) + raise utils.HBSDError(msg) + elif (utils.safe_get_err_code(ex.kwargs.get('errobj')) == + rest_api.INVALID_SNAPSHOT_POOL and + not self.conf.hitachi_snap_pool): + msg = utils.output_log( + MSG.INVALID_PARAMETER, param='hitachi_snap_pool') + raise utils.HBSDError(msg) + raise + self._wait_copy_pair_status(pair['svol'], PAIR) + self.client.split_snapshotgroup(snapshotgroup_name) + for pair in pairs: + self._wait_copy_pair_status(pair['svol'], PSUS) + except Exception: + with excutils.save_and_reraise_exception(): + self._delete_pairs_from_storage(pairs) + + def _create_cgsnapshot(self, context, cgsnapshot, snapshots): + pairs = [] + events = [] + snapshots_model_update = [] + + def _create_cgsnapshot_volume(snapshot): + pair = {'snapshot': snapshot} + try: + pair['pvol'] = utils.get_ldev(snapshot.volume) + if pair['pvol'] is None: + msg = utils.output_log( + MSG.INVALID_LDEV_FOR_VOLUME_COPY, + type='volume', id=snapshot.volume_id) + raise utils.HBSDError(msg) + size = snapshot.volume_size + pair['svol'] = self.create_ldev(size) + except Exception as exc: + pair['msg'] = utils.get_exception_msg(exc) + raise loopingcall.LoopingCallDone(pair) + + try: + for snapshot in snapshots: + ldev = utils.get_ldev(snapshot.volume) + if ldev is None: + msg = utils.output_log( + MSG.INVALID_LDEV_FOR_VOLUME_COPY, type='volume', + id=snapshot.volume_id) + raise utils.HBSDError(msg) + for snapshot in snapshots: + loop = loopingcall.FixedIntervalLoopingCall( + _create_cgsnapshot_volume, snapshot) + event = loop.start(interval=0) + events.append(event) + is_success = True + for e in events: + pair = e.wait() + if 'msg' in pair: + is_success = False + msg = pair['msg'] + pairs.append(pair) + if not is_success: + raise utils.HBSDError(msg) + self._create_ctg_snap_pair(pairs) + except Exception: + for pair in pairs: + if 'svol' in pair and pair['svol'] is not None: + try: + self.delete_ldev(pair['svol']) + except utils.HBSDError: + utils.output_log( + MSG.DELETE_LDEV_FAILED, ldev=pair['svol']) + model_update = {'status': fields.GroupSnapshotStatus.ERROR} + for snapshot in snapshots: + snapshot_model_update = {'id': snapshot.id, + 'status': fields.SnapshotStatus.ERROR} + snapshots_model_update.append(snapshot_model_update) + return model_update, snapshots_model_update + for pair in pairs: + snapshot_model_update = { + 'id': pair['snapshot'].id, + 'status': fields.SnapshotStatus.AVAILABLE, + 'provider_location': str(pair['svol'])} + snapshots_model_update.append(snapshot_model_update) + return None, snapshots_model_update + + def create_group_snapshot(self, context, group_snapshot, snapshots): + if volume_utils.is_group_a_cg_snapshot_type(group_snapshot): + return self._create_cgsnapshot(context, group_snapshot, snapshots) + else: + return self._create_non_cgsnapshot(group_snapshot, snapshots) diff --git a/cinder/volume/drivers/hitachi/hbsd_rest_api.py b/cinder/volume/drivers/hitachi/hbsd_rest_api.py index 237535d2af7..82a10fc180e 100644 --- a/cinder/volume/drivers/hitachi/hbsd_rest_api.py +++ b/cinder/volume/drivers/hitachi/hbsd_rest_api.py @@ -750,6 +750,14 @@ class RestApiClient(): } self._invoke(url, body=body) + def split_snapshotgroup(self, snapshot_group_id): + url = '%(url)s/snapshot-groups/%(id)s/actions/%(action)s/invoke' % { + 'url': self.object_url, + 'id': snapshot_group_id, + 'action': 'split', + } + self._invoke(url) + def discard_zero_page(self, ldev_id): """Return the ldev's no-data pages to the storage pool.""" url = '%(url)s/ldevs/%(id)s/actions/%(action)s/invoke' % { diff --git a/cinder/volume/drivers/hitachi/hbsd_utils.py b/cinder/volume/drivers/hitachi/hbsd_utils.py index 4810f1b34b6..fb34e1c4271 100644 --- a/cinder/volume/drivers/hitachi/hbsd_utils.py +++ b/cinder/volume/drivers/hitachi/hbsd_utils.py @@ -344,6 +344,21 @@ class HBSDMsg(enum.Enum): 'to manage the volume.', 'suffix': ERROR_SUFFIX, } + FAILED_CREATE_CTG_SNAPSHOT = { + 'msg_id': 712, + 'loglevel': base_logging.ERROR, + 'msg': 'Failed to create a consistency group snapshot. ' + 'The number of pairs in the consistency group or the number of ' + 'consistency group snapshots has reached the limit.', + 'suffix': ERROR_SUFFIX, + } + LDEV_NOT_EXIST_FOR_ADD_GROUP = { + 'msg_id': 716, + 'loglevel': base_logging.ERROR, + 'msg': 'No logical device exists in the storage system for the volume ' + '%(volume_id)s to be added to the %(group)s %(group_id)s.', + 'suffix': ERROR_SUFFIX, + } SNAPSHOT_UNMANAGE_FAILED = { 'msg_id': 722, 'loglevel': base_logging.ERROR, @@ -395,6 +410,23 @@ class HBSDMsg(enum.Enum): 'body: %(body)s)', 'suffix': ERROR_SUFFIX, } + GROUP_OBJECT_DELETE_FAILED = { + 'msg_id': 736, + 'loglevel': base_logging.ERROR, + 'msg': 'Failed to delete a %(obj)s in a %(group)s. (%(group)s: ' + '%(group_id)s, %(obj)s: %(obj_id)s, LDEV: %(ldev)s, reason: ' + '%(reason)s)', + 'suffix': ERROR_SUFFIX, + } + GROUP_SNAPSHOT_CREATE_FAILED = { + 'msg_id': 737, + 'loglevel': base_logging.ERROR, + 'msg': 'Failed to create a volume snapshot in a group snapshot that ' + 'does not guarantee consistency. (group: %(group)s, ' + 'group snapshot: %(group_snapshot)s, group type: ' + '%(group_type)s, volume: %(volume)s, snapshot: %(snapshot)s)', + 'suffix': ERROR_SUFFIX, + } def __init__(self, error_info): """Initialize Enum attributes.""" @@ -526,3 +558,20 @@ def is_shared_connection(volume, connector): if attachment.attached_host == host: connection_count += 1 return connection_count > 1 + + +def cleanup_cg_in_volume(volume): + if ('group_id' in volume and volume.group_id and + 'consistencygroup_id' in volume and + volume.consistencygroup_id): + volume.consistencygroup_id = None + if 'consistencygroup' in volume: + volume.consistencygroup = None + + +def get_exception_msg(exc): + if exc.args: + return exc.msg if isinstance( + exc, exception.CinderException) else exc.args[0] + else: + return "" diff --git a/doc/source/configuration/block-storage/drivers/hitachi-vsp-driver.rst b/doc/source/configuration/block-storage/drivers/hitachi-vsp-driver.rst index 2b3c400892c..76548697112 100644 --- a/doc/source/configuration/block-storage/drivers/hitachi-vsp-driver.rst +++ b/doc/source/configuration/block-storage/drivers/hitachi-vsp-driver.rst @@ -63,6 +63,8 @@ Supported operations * Create, delete, attach, and detach volumes. * Create, list, and delete volume snapshots. * Create a volume from a snapshot. +* Create, list, update, and delete consistency groups. +* Create, list, and delete consistency group snapshots. * Copy a volume to an image. * Copy an image to a volume. * Clone a volume. diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index 128d353ea80..12d6b50089e 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -587,7 +587,7 @@ driver.dell_emc_vnx=complete driver.dell_emc_powerflex=complete driver.dell_emc_xtremio=complete driver.fujitsu_eternus=missing -driver.hitachi_vsp=missing +driver.hitachi_vsp=complete driver.hpe_3par=complete driver.hpe_msa=missing driver.huawei_t_v1=missing diff --git a/releasenotes/notes/hitachi-generic-volume-groups-434a27b290d51bf3.yaml b/releasenotes/notes/hitachi-generic-volume-groups-434a27b290d51bf3.yaml new file mode 100644 index 00000000000..720f051aaed --- /dev/null +++ b/releasenotes/notes/hitachi-generic-volume-groups-434a27b290d51bf3.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Hitachi driver: Add Cinder generic volume groups.