diff --git a/cinder/tests/unit/volume/drivers/test_rsd.py b/cinder/tests/unit/volume/drivers/test_rsd.py new file mode 100644 index 00000000000..c37adca3983 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/test_rsd.py @@ -0,0 +1,1276 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +import mock +import sys + +from cinder import exception +from cinder.i18n import _ +from cinder import test +from cinder.tests.unit.volume import test_driver + + +MOCK_URL = "http://www.mock.url.com:4242" +MOCK_USER = "mock_user" +MOCK_PASSWORD = "mock_password" + + +class MockHTTPError(Exception): + def __init__(self, msg): + super(MockHTTPError, self).__init__(msg) + + +class MockConnectionError(Exception): + def __init__(self, msg): + super(MockConnectionError, self).__init__(msg) + + +class MockResourceNotFoundError(Exception): + def __init__(self, msg): + super(MockResourceNotFoundError, self).__init__(msg) + + +class MockBadRequestError(Exception): + def __init__(self, msg): + super(MockBadRequestError, self).__init__(msg) + self.body = { + "@Message.ExtendedInfo": + [{"Message": + "Cannot delete source snapshot volume when " + "other clone volumes are based on this snapshot."}]} + + +class MockInvalidParameterValueError(Exception): + def __init__(self, msg): + super(MockInvalidParameterValueError, self).__init__(msg) + + +fake_RSDLib = mock.Mock() +fake_rsd_lib = mock.Mock() +fake_rsd_lib.RSDLib = mock.MagicMock(return_value=fake_RSDLib) +fake_sushy = mock.Mock() +fake_sushy.exceptions = mock.Mock() +fake_sushy.exceptions.HTTPError = MockHTTPError +fake_sushy.exceptions.ConnectionError = MockConnectionError +fake_sushy.exceptions.ResourceNotFoundError = MockResourceNotFoundError +fake_sushy.exceptions.BadRequestError = MockBadRequestError +fake_sushy.exceptions.InvalidParameterValueError = ( + MockInvalidParameterValueError) + +sys.modules['rsd_lib'] = fake_rsd_lib +sys.modules['sushy'] = fake_sushy + +from cinder.volume.drivers import rsd as rsd_driver + + +class RSDClientTestCase(test.TestCase): + + def setUp(self): + super(RSDClientTestCase, self).setUp() + self.mock_rsd_lib = mock.Mock() + self.mock_rsd_lib._rsd_api_version = "2.4.0" + self.mock_rsd_lib_factory = mock.MagicMock( + return_value=self.mock_rsd_lib) + fake_RSDLib.factory = self.mock_rsd_lib_factory + + self.rsd_client = rsd_driver.RSDClient(self.mock_rsd_lib) + self.uuid = "84cff9ea-de0f-4841-8645-58620adf49b2" + self.url = "/redfish/v1/Resource/Type" + self.resource_url = self.url + "/" + self.uuid + + def _generate_rsd_storage_objects(self): + self._mock_stor_obj_1 = mock.Mock() + self._mock_stor_obj_2 = mock.Mock() + self._mock_stor_obj_3 = mock.Mock() + + self._mock_drive_obj_1 = mock.Mock() + self._mock_drive_obj_2 = mock.Mock() + self._mock_drive_obj_3 = mock.Mock() + + self._mock_drive_obj_1.protocol = "NVMe" + self._mock_drive_obj_2.protocol = "Blank" + self._mock_drive_obj_3.protocol = "" + + self._mock_stor_obj_1.drives.get_members = mock.MagicMock( + return_value=[self._mock_drive_obj_1]) + + self._mock_stor_obj_2.drives.get_members = mock.MagicMock( + return_value=[self._mock_drive_obj_2]) + + self._mock_stor_obj_3.drives.get_members = mock.MagicMock( + return_value=[self._mock_drive_obj_3]) + + self._mock_stor_collection = [self._mock_stor_obj_1, + self._mock_stor_obj_2, + self._mock_stor_obj_3] + + def test_initialize(self): + rsd_client = rsd_driver.RSDClient.initialize(MOCK_URL, MOCK_USER, + MOCK_PASSWORD, + verify=True) + self.assertTrue(isinstance(rsd_client, rsd_driver.RSDClient)) + + def test_initialize_incorrect_version(self): + self.mock_rsd_lib._rsd_api_version = "2.3.0" + rsd_client_init = rsd_driver.RSDClient.initialize + self.assertRaises(exception.VolumeBackendAPIException, + rsd_client_init, MOCK_URL, MOCK_USER, + MOCK_PASSWORD, False) + + def test_initialize_higher_version(self): + self.mock_rsd_lib._rsd_api_version = "2.5.0" + rsd_client = rsd_driver.RSDClient.initialize(MOCK_URL, MOCK_USER, + MOCK_PASSWORD, + verify=True) + self.assertTrue(isinstance(rsd_client, rsd_driver.RSDClient)) + + def test_initialize_invalid_credentials(self): + self.mock_rsd_lib_factory.side_effect = ( + fixtures._fixtures.timeout.TimeoutException) + rsd_client_init = rsd_driver.RSDClient.initialize + self.assertRaises(exception.VolumeBackendAPIException, + rsd_client_init, MOCK_URL, MOCK_USER, + MOCK_PASSWORD, False) + + def test_get_storage(self): + mock_stor_serv = mock.Mock() + self.mock_rsd_lib.get_storage_service = mock.MagicMock( + return_value=mock_stor_serv) + + stor_serv = self.rsd_client._get_storage(self.resource_url) + + self.assertEqual(mock_stor_serv, stor_serv) + self.mock_rsd_lib.get_storage_service.assert_called_with(self.url) + + def test_get_storages(self): + self._generate_rsd_storage_objects() + get_mem = self.mock_rsd_lib.get_storage_service_collection.return_value + get_mem.get_members.return_value = self._mock_stor_collection + + storages = self.rsd_client._get_storages() + + self.assertEqual([self._mock_stor_obj_1], storages) + + def test_get_storages_non_nvme(self): + self._generate_rsd_storage_objects() + get_mem = self.mock_rsd_lib.get_storage_service_collection.return_value + get_mem.get_members.return_value = self._mock_stor_collection + + storages = self.rsd_client._get_storages(False) + + self.assertEqual([self._mock_stor_obj_1, self._mock_stor_obj_2, + self._mock_stor_obj_3], storages) + + def test_get_storages_empty_storage(self): + self._generate_rsd_storage_objects() + get_mem = self.mock_rsd_lib.get_storage_service_collection.return_value + get_mem.get_members.return_value = [] + + storages = self.rsd_client._get_storages() + + self.assertEqual([], storages) + + def test_get_storages_empty_drive(self): + self._generate_rsd_storage_objects() + get_mem = self.mock_rsd_lib.get_storage_service_collection.return_value + get_mem.get_members.return_value = self._mock_stor_collection + + self._mock_stor_obj_1.drives.get_members = mock.MagicMock( + return_value=[]) + + storages = self.rsd_client._get_storages() + + self.assertEqual([], storages) + + def test_get_volume(self): + mock_stor_serv = mock.Mock() + mock_vol_serv = mock.Mock() + self.mock_rsd_lib.get_storage_service = mock.MagicMock( + return_value=mock_stor_serv) + mock_stor_serv.volumes.get_member = mock.MagicMock( + return_value=mock_vol_serv) + + vol_serv = self.rsd_client._get_volume(self.resource_url) + + self.assertEqual(mock_vol_serv, vol_serv) + self.mock_rsd_lib.get_storage_service.assert_called_with(self.url) + mock_stor_serv.volumes.get_member.assert_called_with(self.resource_url) + + def test_get_providing_pool(self): + mock_volume = mock.Mock() + mock_volume.capacity_sources = [mock.Mock()] + mock_volume.capacity_sources[0].providing_pools = [mock.Mock()] + + provider_pool = self.rsd_client._get_providing_pool(mock_volume) + + self.assertEqual(mock_volume.capacity_sources[0].providing_pools[0], + provider_pool) + + def test_get_providing_pool_no_capacity(self): + mock_volume = mock.Mock() + mock_volume.capacity_sources = [] + + self.assertRaises(exception.ValidationError, + self.rsd_client._get_providing_pool, + mock_volume) + + def test_get_providing_pool_no_pools(self): + mock_volume = mock.Mock() + mock_volume.capacity_sources = [mock.Mock()] + mock_volume.capacity_sources[0].providing_pools = [] + + self.assertRaises(exception.ValidationError, + self.rsd_client._get_providing_pool, + mock_volume) + + def test_get_providing_pool_too_many_pools(self): + mock_volume = mock.Mock() + mock_volume.capacity_sources = [mock.Mock()] + mock_volume.capacity_sources[0].providing_pools = [mock.Mock(), + mock.Mock()] + + self.assertRaises(exception.ValidationError, + self.rsd_client._get_providing_pool, + mock_volume) + + def test_create_vol_or_snap(self): + mock_stor = mock.Mock() + size_in_bytes = 10737418240 + mock_stor.volumes.create_volume = mock.Mock( + return_value=self.resource_url) + + stor_url = self.rsd_client._create_vol_or_snap(mock_stor, + size_in_bytes) + + self.assertEqual(self.resource_url, stor_url) + mock_stor.volumes.create_volume.assert_called_with( + size_in_bytes, capacity_sources=None, replica_infos=None) + + def test_create_vol_or_snap_stor_pool(self): + mock_stor = mock.Mock() + size_in_bytes = 10737418240 + stor_uuid = "/redfish/v1/StorageService/NvMeoE1/StoragePools/2" + expected_capacity = [{ + "ProvidingPools": [{ + "@odata.id": stor_uuid + }] + }] + mock_stor.volumes.create_volume = mock.Mock( + return_value=self.resource_url) + + stor_url = self.rsd_client._create_vol_or_snap(mock_stor, + size_in_bytes, + pool_url=stor_uuid) + + self.assertEqual(self.resource_url, stor_url) + mock_stor.volumes.create_volume.assert_called_with( + size_in_bytes, + capacity_sources=expected_capacity, + replica_infos=None) + + def test_create_vol_or_snap_source_snap(self): + mock_stor = mock.Mock() + size_in_bytes = 10737418240 + stor_uuid = "/redfish/v1/StorageService/NvMeoE1/StoragePools/2" + expected_replica = [{ + "ReplicaType": "Clone", + "Replica": {"@odata.id": stor_uuid} + }] + mock_stor.volumes.create_volume = mock.Mock( + return_value=self.resource_url) + + stor_url = self.rsd_client._create_vol_or_snap(mock_stor, + size_in_bytes, + source_snap=stor_uuid) + + self.assertEqual(self.resource_url, stor_url) + mock_stor.volumes.create_volume.assert_called_with( + size_in_bytes, + capacity_sources=None, + replica_infos=expected_replica) + + def test_create_vol_or_snap_source_vol(self): + mock_stor = mock.Mock() + size_in_bytes = 10737418240 + stor_uuid = "/redfish/v1/StorageService/NvMeoE1/StoragePools/2" + expected_replica = [{ + "ReplicaType": "Snapshot", + "Replica": {"@odata.id": stor_uuid} + }] + mock_stor.volumes.create_volume = mock.Mock( + return_value=self.resource_url) + + stor_url = self.rsd_client._create_vol_or_snap(mock_stor, + size_in_bytes, + source_vol=stor_uuid) + + self.assertEqual(self.resource_url, stor_url) + mock_stor.volumes.create_volume.assert_called_with( + size_in_bytes, + capacity_sources=None, + replica_infos=expected_replica) + + def test_create_vol_or_snap_source_snap_vol(self): + mock_stor = mock.Mock() + size_in_bytes = 10737418240 + stor_uuid = "/redfish/v1/StorageService/NvMeoE1/StoragePools/2" + mock_stor.volumes.create_volume = mock.Mock( + return_value=self.resource_url) + + self.assertRaises(exception.InvalidInput, + self.rsd_client._create_vol_or_snap, + mock_stor, size_in_bytes, source_snap=stor_uuid, + source_vol=stor_uuid) + + def test_create_volume(self): + self._generate_rsd_storage_objects() + size_in_Gb = 10 + expected_size_in_bytes = 10737418240 + self._mock_stor_obj_1.volumes.create_volume = mock.Mock( + return_value=self.resource_url) + self.rsd_client._get_storages = mock.Mock( + return_value=[self._mock_stor_obj_1]) + + stor_url = self.rsd_client.create_volume(size_in_Gb) + + self._mock_stor_obj_1.volumes.create_volume.assert_called_with( + expected_size_in_bytes, capacity_sources=None, replica_infos=None) + self.assertEqual(self.resource_url, stor_url) + + def test_create_volume_no_storage(self): + self._generate_rsd_storage_objects() + size_in_Gb = 10 + self.rsd_client._get_storages = mock.Mock( + return_value=[]) + + self.assertRaises(exception.VolumeBackendAPIException, + self.rsd_client.create_volume, size_in_Gb) + + def test_create_volume_multiple_storages(self): + self._generate_rsd_storage_objects() + size_in_Gb = 10 + expected_size_in_bytes = 10737418240 + mock_resp = mock.Mock() + mock_resp.status = "404" + self._mock_stor_obj_1.volumes.create_volume = mock.Mock( + return_value=self.resource_url) + self._mock_stor_obj_2.volumes.create_volume = mock.Mock( + side_effect=MockHTTPError("HTTP Error")) + self._mock_stor_obj_3.volumes.create_volume = mock.Mock( + side_effect=MockConnectionError("Connection Error")) + self.rsd_client._get_storages = mock.Mock( + return_value=[self._mock_stor_obj_3, + self._mock_stor_obj_2, + self._mock_stor_obj_1]) + + stor_url = self.rsd_client.create_volume(size_in_Gb) + + self._mock_stor_obj_1.volumes.create_volume.assert_called_with( + expected_size_in_bytes, capacity_sources=None, + replica_infos=None) + self.assertEqual(self.resource_url, stor_url) + + def test_clone_volume(self): + mock_volume = mock.Mock() + mock_volume.capacity_bytes = 10737418240 + mock_volume.capacity_sources = [mock.Mock()] + mock_volume.capacity_sources[0].providing_pools = [mock.Mock()] + mock_storage = mock.Mock() + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_storage = mock.Mock(return_value=mock_storage) + self.rsd_client._create_vol_or_snap = mock.Mock( + return_value=self.resource_url) + self.rsd_client._get_providing_pool = mock.Mock( + return_value=self.resource_url) + + vol_url, snap_url = self.rsd_client.clone_volume(self.resource_url) + + self.assertEqual(self.resource_url, vol_url) + self.assertEqual(self.resource_url, snap_url) + self.rsd_client._create_vol_or_snap.assert_called_with( + mock.ANY, 10737418240, pool_url=self.resource_url, + source_snap=self.resource_url) + + def test_clone_volume_size_increase(self): + mock_volume = mock.Mock() + mock_volume.capacity_bytes = 10737418240 + new_size = 20 + mock_volume.capacity_sources = [mock.Mock()] + mock_volume.capacity_sources[0].providing_pools = [mock.Mock()] + mock_storage = mock.Mock() + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_storage = mock.Mock(return_value=mock_storage) + self.rsd_client._create_vol_or_snap = mock.Mock( + return_value=self.resource_url) + self.rsd_client._get_providing_pool = mock.Mock( + return_value=self.resource_url) + + vol_url, snap_url = self.rsd_client.clone_volume(self.resource_url, + new_size) + + self.assertEqual(self.resource_url, vol_url) + self.assertEqual(self.resource_url, snap_url) + self.rsd_client._create_vol_or_snap.assert_called_with( + mock.ANY, 21474836480, pool_url=self.resource_url, + source_snap=self.resource_url) + + def test_clone_volume_fail(self): + mock_volume = mock.Mock() + mock_volume.capacity_bytes = 10737418240 + mock_volume.capacity_sources = [mock.Mock()] + mock_volume.capacity_sources[0].providing_pools = [mock.Mock()] + mock_storage = mock.Mock() + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_storage = mock.Mock(return_value=mock_storage) + self.rsd_client.delete_vol_or_snap = mock.Mock() + self.rsd_client._create_vol_or_snap = mock.Mock( + return_value=self.resource_url, + side_effect=[None, exception.InvalidInput( + reason=(_("_create_vol_or_snap failed")))]) + self.rsd_client._get_providing_pool = mock.Mock( + return_value=self.resource_url) + + self.assertRaises(exception.VolumeBackendAPIException, + self.rsd_client.clone_volume, + self.resource_url) + self.rsd_client.delete_vol_or_snap.assert_called_once() + + def test_create_volume_from_snap(self): + mock_snap = mock.Mock() + mock_storage = mock.Mock() + mock_snap.capacity_bytes = 10737418240 + self.rsd_client._get_storage = mock.Mock(return_value=mock_storage) + self.rsd_client._get_volume = mock.Mock(return_value=mock_snap) + self.rsd_client._get_providing_pool = mock.Mock( + return_value=self.resource_url) + self.rsd_client._create_vol_or_snap = mock.Mock( + return_value=self.resource_url) + + volume_url = self.rsd_client.create_volume_from_snap(self.resource_url) + + self.assertEqual(self.resource_url, volume_url) + self.rsd_client._create_vol_or_snap.assert_called_with( + mock.ANY, + 10737418240, + pool_url=self.resource_url, + source_snap=self.resource_url) + + def test_create_volume_from_snap_with_size(self): + mock_snap = mock.Mock() + mock_storage = mock.Mock() + mock_snap.capacity_bytes = 10737418240 + expected_capacity_bytes = 21474836480 + self.rsd_client._get_storage = mock.Mock(return_value=mock_storage) + self.rsd_client._get_volume = mock.Mock(return_value=mock_snap) + self.rsd_client._get_providing_pool = mock.Mock( + return_value=self.resource_url) + self.rsd_client._create_vol_or_snap = mock.Mock( + return_value=self.resource_url) + + volume_url = self.rsd_client.create_volume_from_snap( + self.resource_url, 20) + + self.assertEqual(self.resource_url, volume_url) + self.rsd_client._create_vol_or_snap.assert_called_with( + mock.ANY, + expected_capacity_bytes, + pool_url=self.resource_url, + source_snap=self.resource_url) + + def test_create_volume_from_snap_create_failed(self): + mock_snap = mock.Mock() + mock_storage = mock.Mock() + mock_snap.capacity_bytes = 10737418240 + self.rsd_client._get_storage = mock.Mock(return_value=mock_storage) + self.rsd_client._get_volume = mock.Mock(return_value=mock_snap) + self.rsd_client._get_providing_pool = mock.Mock( + return_value=self.resource_url) + self.rsd_client._create_vol_or_snap = mock.Mock( + return_value=self.resource_url, + side_effect=[exception.InvalidInput( + reason=_("_create_vol_or_snap failed."))]) + + self.assertRaises( + exception.VolumeBackendAPIException, + self.rsd_client.create_volume_from_snap, + self.resource_url) + + def test_delete_vol_or_snap(self): + mock_volume = mock.Mock() + mock_volume.links.endpoints = [] + mock_volume.delete = mock.Mock() + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + + self.rsd_client.delete_vol_or_snap(self.resource_url) + + mock_volume.delete.assert_called_once() + + def test_delete_vol_or_snap_failed_delete(self): + mock_volume = mock.Mock() + mock_volume.links.endpoints = [] + mock_volume.delete = mock.Mock(side_effect=[ + RuntimeError("delete error")]) + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + + self.assertRaises( + exception.VolumeBackendAPIException, + self.rsd_client.delete_vol_or_snap, + self.resource_url) + + def test_delete_vol_or_snap_non_exist(self): + mock_volume = mock.Mock() + mock_volume.links.endpoints = [] + mock_volume.delete = mock.Mock() + self.rsd_client._get_volume = mock.Mock( + side_effect=MockResourceNotFoundError("volume doesn't exist!")) + + self.assertRaises(exception.VolumeBackendAPIException, + self.rsd_client.delete_vol_or_snap, + self.resource_url, + ignore_non_exist=True) + mock_volume.delete.assert_not_called() + + def test_delete_vol_or_snap_has_endpoints(self): + mock_volume = mock.Mock() + mock_volume.links.endpoints = [mock.Mock()] + mock_volume.delete = mock.Mock() + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + + self.assertRaises(exception.VolumeBackendAPIException, + self.rsd_client.delete_vol_or_snap, + self.resource_url) + mock_volume.delete.assert_not_called() + + def test_delete_vol_or_snap_has_deps(self): + mock_volume = mock.Mock() + mock_volume.links.endpoints = [mock.Mock()] + mock_volume.delete = mock.Mock( + side_effect=MockBadRequestError("busy!")) + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client.delete_vol_or_snap = mock.Mock( + side_effect=[None, exception.VolumeBackendAPIException( + data="error")]) + + self.rsd_client.delete_vol_or_snap(self.resource_url) + self.rsd_client.delete_vol_or_snap.assert_called_once() + + def test_attach_volume_to_node_invalid_vol_url(self): + self.rsd_client._get_volume = mock.Mock(side_effect=[ + RuntimeError("_get_volume failed")]) + self.rsd_client._get_node = mock.Mock() + + self.assertRaises( + exception.VolumeBackendAPIException, + self.rsd_client.attach_volume_to_node, + self.resource_url, + self.resource_url) + self.rsd_client._get_volume.assert_called_once() + self.rsd_client._get_node.assert_not_called() + + def test_attach_volume_to_node_invalid_node_url(self): + mock_volume = mock.Mock() + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(side_effect=[ + RuntimeError("_get_node failed")]) + + self.assertRaises( + exception.VolumeBackendAPIException, + self.rsd_client.attach_volume_to_node, + self.resource_url, + self.resource_url) + self.rsd_client._get_volume.assert_called_once() + self.rsd_client._get_node.assert_called_once() + + def test_attach_volume_to_node_already_attached(self): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_volume.links.endpoints = [mock.Mock()] + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + + self.assertRaises( + exception.VolumeBackendAPIException, + self.rsd_client.attach_volume_to_node, + self.resource_url, + self.resource_url) + self.rsd_client._get_volume.assert_called_once() + self.rsd_client._get_node.assert_called_once() + + @mock.patch('time.sleep') + def test_attach_volume_to_node_too_few_endpoints(self, mock_sleep): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_volume.links.endpoints = [] + mock_node.detach_endpoint = mock.Mock() + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + self.rsd_client._get_nqn_endpoints = mock.Mock(return_value=[]) + + self.assertRaises( + rsd_driver.RSDRetryableException, + self.rsd_client.attach_volume_to_node, + self.resource_url, + self.resource_url) + self.assertEqual(5, mock_node.attach_endpoint.call_count) + self.assertEqual(5, mock_node.detach_endpoint.call_count) + + @mock.patch('time.sleep') + def test_attach_volume_to_node_too_many_endpoints(self, mock_sleep): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_volume.links.endpoints = [] + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + self.rsd_client._get_nqn_endpoints = mock.Mock(return_value=[ + mock.Mock(), + mock.Mock()]) + + self.assertRaises( + rsd_driver.RSDRetryableException, + self.rsd_client.attach_volume_to_node, + self.resource_url, + self.resource_url) + self.assertEqual(5, mock_node.attach_endpoint.call_count) + self.assertEqual(5, mock_node.detach_endpoint.call_count) + + @mock.patch('time.sleep') + def test_attach_volume_to_node_too_few_ip_transport(self, mock_sleep): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_target_nqn = mock.Mock() + v_endpoints = {"IPTransportDetails": []} + mock_v_endpoints = [(mock_target_nqn, v_endpoints)] + mock_volume.links.endpoints = [] + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + self.rsd_client._get_nqn_endpoints = mock.Mock( + return_value=mock_v_endpoints) + + self.assertRaises( + rsd_driver.RSDRetryableException, + self.rsd_client.attach_volume_to_node, + self.resource_url, + self.resource_url) + self.assertEqual(5, mock_node.attach_endpoint.call_count) + self.assertEqual(5, mock_node.detach_endpoint.call_count) + + @mock.patch('time.sleep') + def test_attach_volume_to_node_too_many_ip_transport(self, mock_sleep): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_target_nqn = mock.Mock() + v_endpoints = {"IPTransportDetails": [mock.Mock(), mock.Mock()]} + mock_v_endpoints = [(mock_target_nqn, v_endpoints)] + mock_volume.links.endpoints = [] + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + self.rsd_client._get_nqn_endpoints = mock.Mock( + return_value=mock_v_endpoints) + + self.assertRaises( + rsd_driver.RSDRetryableException, + self.rsd_client.attach_volume_to_node, + self.resource_url, + self.resource_url) + self.assertEqual(5, mock_node.attach_endpoint.call_count) + self.assertEqual(5, mock_node.detach_endpoint.call_count) + + @mock.patch('time.sleep') + def test_attach_volume_to_node_no_n_endpoints(self, mock_sleep): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_target_nqn = mock.Mock() + mock_ip = '0.0.0.0' + mock_port = 5446 + target_ip = {"Address": mock_ip} + ip_transport = {"IPv4Address": target_ip, "Port": mock_port} + v_endpoints = {"IPTransportDetails": [ip_transport]} + mock_v_endpoints = [(mock_target_nqn, v_endpoints)] + mock_volume.links.endpoints = [] + mock_node_system = mock.Mock() + mock_node_system.json = {"Links": {"Endpoints": []}} + self.mock_rsd_lib.get_system = mock.MagicMock( + return_value=mock_node_system) + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + self.rsd_client._get_nqn_endpoints = mock.Mock(side_effect=[ + mock_v_endpoints, [], + mock_v_endpoints, [], + mock_v_endpoints, [], + mock_v_endpoints, [], + mock_v_endpoints, []]) + + self.assertRaises( + rsd_driver.RSDRetryableException, + self.rsd_client.attach_volume_to_node, + self.resource_url, + self.resource_url) + self.assertEqual(5, mock_node.attach_endpoint.call_count) + self.assertEqual(5, mock_node.detach_endpoint.call_count) + + @mock.patch('time.sleep') + def test_attach_volume_to_node_retry_attach(self, mock_sleep): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_target_nqn = mock.Mock() + mock_ip = '0.0.0.0' + mock_port = 5446 + mock_host_nqn = 'host_nqn' + target_ip = {"Address": mock_ip} + ip_transport = {"IPv4Address": target_ip, "Port": mock_port} + v_endpoints = {"IPTransportDetails": [ip_transport]} + mock_v_endpoints = [(mock_target_nqn, v_endpoints)] + mock_n_endpoints = [(mock_host_nqn, v_endpoints)] + mock_volume.links.endpoints = [] + mock_node_system = mock.Mock() + mock_node_system.json = {"Links": {"Endpoints": []}} + self.mock_rsd_lib.get_system = mock.MagicMock( + return_value=mock_node_system) + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + self.rsd_client._get_nqn_endpoints = mock.Mock(side_effect=[ + mock_v_endpoints, + mock_n_endpoints]) + mock_node.attach_endpoint = mock.Mock(side_effect=[ + MockInvalidParameterValueError("invalid resource"), None]) + + ret_tuple = self.rsd_client.attach_volume_to_node(self.resource_url, + self.resource_url) + + self.assertEqual((mock_ip, mock_port, mock_target_nqn, + mock_host_nqn), ret_tuple) + self.assertEqual(2, mock_node.attach_endpoint.call_count) + mock_node.detach_endpoint.assert_not_called() + + @mock.patch('time.sleep') + def test_attach_volume_to_node_retry_post_attach(self, mock_sleep): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_target_nqn = mock.Mock() + mock_ip = '0.0.0.0' + mock_port = 5446 + mock_host_nqn = 'host_nqn' + target_ip = {"Address": mock_ip} + ip_transport = {"IPv4Address": target_ip, "Port": mock_port} + v_endpoints = {"IPTransportDetails": [ip_transport]} + mock_v_endpoints = [(mock_target_nqn, v_endpoints)] + mock_n_endpoints = [(mock_host_nqn, v_endpoints)] + mock_volume.links.endpoints = [] + mock_node_system = mock.Mock() + mock_node_system.json = {"Links": {"Endpoints": []}} + self.mock_rsd_lib.get_system = mock.MagicMock( + return_value=mock_node_system) + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + self.rsd_client._get_nqn_endpoints = mock.Mock(side_effect=[ + mock_v_endpoints, + [], + mock_v_endpoints, + mock_n_endpoints]) + + ret_tuple = self.rsd_client.attach_volume_to_node(self.resource_url, + self.resource_url) + + self.assertEqual((mock_ip, mock_port, mock_target_nqn, mock_host_nqn), + ret_tuple) + self.assertEqual(2, mock_node.attach_endpoint.call_count) + mock_node.detach_endpoint.assert_called_once() + + def test_attach_volume_to_node(self): + mock_volume = mock.Mock() + mock_node = mock.Mock() + mock_target_nqn = mock.Mock() + mock_ip = '0.0.0.0' + mock_port = 5446 + mock_host_nqn = 'host_nqn' + target_ip = {"Address": mock_ip} + ip_transport = {"IPv4Address": target_ip, "Port": mock_port} + v_endpoints = {"IPTransportDetails": [ip_transport]} + mock_v_endpoints = [(mock_target_nqn, v_endpoints)] + mock_n_endpoints = [(mock_host_nqn, v_endpoints)] + mock_volume.links.endpoints = [] + mock_node_system = mock.Mock() + mock_node_system.json = {"Links": {"Endpoints": []}} + self.mock_rsd_lib.get_system = mock.MagicMock( + return_value=mock_node_system) + self.rsd_client._get_volume = mock.Mock(return_value=mock_volume) + self.rsd_client._get_node = mock.Mock(return_value=mock_node) + self.rsd_client._get_nqn_endpoints = mock.Mock(side_effect=[ + mock_v_endpoints, + mock_n_endpoints]) + + ret_tuple = self.rsd_client.attach_volume_to_node(self.resource_url, + self.resource_url) + + self.assertEqual((mock_ip, mock_port, mock_target_nqn, mock_host_nqn), + ret_tuple) + mock_node.attach_endpoint.assert_called_once() + mock_node.detach_endpoint.assert_not_called() + + def test_get_node_url_by_uuid(self): + mock_node = mock.Mock() + mock_node.path = self.resource_url + mock_node_system = mock.Mock() + mock_node_system.uuid = self.uuid + self.mock_rsd_lib.get_system = mock.MagicMock( + return_value=mock_node_system) + get_mem = self.mock_rsd_lib.get_node_collection.return_value + get_mem.get_members.return_value = [mock_node] + + node_url = self.rsd_client.get_node_url_by_uuid(self.uuid.lower()) + + self.assertEqual(self.resource_url, node_url) + + def test_get_node_url_by_uuid_uuid_not_present(self): + mock_node = mock.Mock() + mock_node.path = self.resource_url + mock_node_system = mock.Mock() + mock_node_system.uuid = self.uuid + self.mock_rsd_lib.get_system = mock.MagicMock( + return_value=mock_node_system) + get_mem = self.mock_rsd_lib.get_node_collection.return_value + get_mem.get_members.return_value = [] + + node_url = self.rsd_client.get_node_url_by_uuid(self.uuid.lower()) + + self.assertEqual("", node_url) + + def test_get_node_url_by_uuid_multiple_uuids(self): + mock_node = mock.Mock() + mock_node.path = self.resource_url + mock_node_system = mock.Mock() + mock_node_system.uuid = self.uuid + second_uuid = "9f9244dd-59a1-4532-b548-df784c7" + mock_node_dummy = mock.Mock() + mock_node_dummy.path = self.url + "/" + second_uuid + mock_node_dummy_system = mock.Mock() + mock_node_dummy_system.uuid = second_uuid + self.mock_rsd_lib.get_system = mock.MagicMock( + side_effect=[mock_node_dummy_system, mock_node_system]) + get_mem = self.mock_rsd_lib.get_node_collection.return_value + get_mem.get_members.return_value = [mock_node_dummy, mock_node] + + node_url = self.rsd_client.get_node_url_by_uuid(self.uuid.lower()) + + self.assertEqual(self.resource_url, node_url) + + def test_get_node_url_by_uuid_exception(self): + mock_node = mock.Mock() + mock_node.path = self.resource_url + mock_node_system = mock.Mock() + mock_node_system.uuid = self.uuid + self.mock_rsd_lib.get_system = mock.MagicMock( + return_value=mock_node_system) + get_mem = self.mock_rsd_lib.get_node_collection.return_value + get_mem.get_members.side_effect = [RuntimeError("Mock Exception")] + + node_url = self.rsd_client.get_node_url_by_uuid(self.uuid.lower()) + + self.assertEqual("", node_url) + + def test_get_stats(self): + mock_str_pool_1 = mock.Mock() + mock_str_pool_2 = mock.Mock() + mock_str_pool_3 = mock.Mock() + mock_str_pool_1.capacity.allocated_bytes = 10737418240 + mock_str_pool_2.capacity.allocated_bytes = 21474836480 + mock_str_pool_3.capacity.allocated_bytes = 32212254720 + mock_str_pool_1.capacity.consumed_bytes = 5368709120 + mock_str_pool_2.capacity.consumed_bytes = 10737418240 + mock_str_pool_3.capacity.consumed_bytes = 21474836480 + + self._generate_rsd_storage_objects() + self._mock_stor_obj_1.storage_pools.get_members = mock.Mock( + return_value=[mock_str_pool_1]) + self._mock_stor_obj_2.storage_pools.get_members = mock.Mock( + return_value=[mock_str_pool_2]) + self._mock_stor_obj_3.storage_pools.get_members = mock.Mock( + return_value=[mock_str_pool_3]) + self._mock_stor_obj_1.volumes.members_identities = [mock.Mock()] + self._mock_stor_obj_2.volumes.members_identities = [mock.Mock(), + mock.Mock()] + self._mock_stor_obj_3.volumes.members_identities = [mock.Mock(), + mock.Mock(), + mock.Mock()] + self.rsd_client._get_storages = mock.Mock( + return_value=self._mock_stor_collection) + stat_tuple = self.rsd_client.get_stats() + + self.assertEqual((25.0, 60.0, 35.0, 6), stat_tuple) + + def test_get_stats_fail(self): + self.rsd_client._get_storages = mock.Mock() + self.rsd_client._get_storages.side_effect = [ + RuntimeError("Connection Error")] + + stat_tuple = self.rsd_client.get_stats() + + self.assertEqual((0, 0, 0, 0), stat_tuple) + + +class RSDDriverTestCase(test_driver.BaseDriverTestCase): + driver_name = "cinder.volume.drivers.rsd.RSDDriver" + + def setUp(self): + super(RSDDriverTestCase, self).setUp() + self.mock_volume = mock.MagicMock() + self.mock_dict = {'size': 10} + self.volume.driver.rsdClient = mock.MagicMock() + self.rsd_client = self.volume.driver.rsdClient + self.uuid = "84cff9ea-de0f-4841-8645-58620adf49b2" + self.url = "/redfish/v1/Storage/StorageService" + self.resource_url = self.url + "/" + self.uuid + + def test_create_volume(self): + self.rsd_client.create_volume = mock.Mock( + return_value=self.resource_url) + + vol_update = self.volume.driver.create_volume(self.mock_dict) + + self.assertEqual({'provider_location': self.resource_url}, vol_update) + + def test_delete_volume(self): + self.rsd_client.delete_vol_or_snap = mock.Mock( + return_value=True) + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.mock_volume.metadata.get = mock.Mock( + return_value=self.resource_url) + + self.assertIsNone(self.volume.driver.delete_volume(self.mock_volume)) + self.rsd_client.delete_vol_or_snap.assert_called() + self.assertEqual(2, self.rsd_client.delete_vol_or_snap.call_count) + + def test_delete_volume_no_snapshot(self): + self.rsd_client.delete_vol_or_snap = mock.Mock( + return_value=True) + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.mock_volume.metadata.get = mock.Mock(return_value=None) + + self.assertIsNone(self.volume.driver.delete_volume(self.mock_volume)) + self.rsd_client.delete_vol_or_snap.assert_called_once() + + def test_delete_volume_no_volume_url(self): + self.rsd_client.delete_vol_or_snap = mock.Mock( + return_value=True) + self.mock_dict['provider_location'] = None + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + + self.assertIsNone(self.volume.driver.delete_volume(self.mock_volume)) + self.rsd_client.delete_vol_or_snap.assert_not_called() + + def test_delete_volume_busy_volume(self): + self.rsd_client.delete_vol_or_snap = mock.Mock( + side_effect=[exception.VolumeIsBusy( + volume_name=self.mock_volume.name)]) + + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + + self.assertRaises(exception.VolumeIsBusy, + self.volume.driver.delete_volume, self.mock_volume) + self.rsd_client.delete_vol_or_snap.assert_called_once() + + def test_delete_volume_snap_deletion_error(self): + self.rsd_client.delete_vol_or_snap = mock.Mock( + side_effect=[None, exception.VolumeBackendAPIException( + data="error")]) + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + + self.assertRaises(exception.VolumeBackendAPIException, + self.volume.driver.delete_volume, self.mock_volume) + self.rsd_client.delete_vol_or_snap.assert_called() + self.assertEqual(2, self.rsd_client.delete_vol_or_snap.call_count) + + def test_get_volume_stats(self): + ret_tuple = (25.0, 60.0, 35.0, 6) + self.rsd_client.get_stats = mock.Mock(return_value=ret_tuple) + + stats = self.volume.driver.get_volume_stats() + + self.assertEqual({}, stats) + + def test_get_volume_stats_refresh(self): + ret_tuple = (25.0, 60.0, 35.0, 6) + self.rsd_client.get_stats = mock.Mock(return_value=ret_tuple) + expected_stats = {'driver_version': '1.0.0', + 'pools': [{ + 'allocated_capacity_gb': 35.0, + 'free_capacity_gb': 25.0, + 'multiattach': False, + 'pool_name': 'RSD', + 'thick_provisioning_support': True, + 'thin_provisioning_support': True, + 'total_capacity_gb': 60.0}], + 'storage_protocol': 'nvmeof', + 'vendor_name': 'Intel', + 'volume_backend_name': 'RSD'} + + stats = self.volume.driver.get_volume_stats(refresh=True) + self.assertEqual(expected_stats, stats) + + def test_initialize_connection(self): + mock_connector = {'system uuid': + "281bbc50-e76f-40e7-a757-06b916a83d6f"} + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.rsd_client.get_node_url_by_uuid = mock.Mock( + return_value=self.resource_url) + ret_tuple = ("0.0.0.0", 5467, "target.mock.nqn", "initiator.mock.nqn") + self.rsd_client.attach_volume_to_node = mock.Mock( + return_value=ret_tuple) + expected_conn_info = { + 'driver_volume_type': 'nvmeof', + 'data': { + 'transport_type': 'rdma', + 'host_nqn': "initiator.mock.nqn", + 'nqn': "target.mock.nqn", + 'target_port': 5467, + 'target_portal': "0.0.0.0", + } + } + + conn_info = self.volume.driver.initialize_connection(self.mock_volume, + mock_connector) + + self.assertEqual(expected_conn_info, conn_info) + + def test_initialize_connection_node_not_found(self): + mock_connector = {'system uuid': + "281bbc50-e76f-40e7-a757-06b916a83d6f"} + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.rsd_client.get_node_url_by_uuid = mock.Mock( + return_value="") + ret_tuple = ("0.0.0.0", 5467, "target.mock.nqn", "initiator.mock.nqn") + self.rsd_client.attach_volume_to_node = mock.Mock( + return_value=ret_tuple) + + self.assertRaises(exception.VolumeBackendAPIException, + self.volume.driver.initialize_connection, + self.mock_volume, mock_connector) + self.rsd_client.attach_volume_to_node.assert_not_called() + self.rsd_client.get_node_url_by_uuid.assert_called_once() + + def test_initialize_connection_no_system_uuid(self): + mock_connector = {} + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.rsd_client.get_node_url_by_uuid = mock.Mock( + return_value=self.resource_url) + ret_tuple = ("0.0.0.0", 5467, "target.mock.nqn", "initiator.mock.nqn") + self.rsd_client.attach_volume_to_node = mock.Mock( + return_value=ret_tuple) + + self.assertRaises(exception.VolumeBackendAPIException, + self.volume.driver.initialize_connection, + self.mock_volume, mock_connector) + self.rsd_client.attach_volume_to_node.assert_not_called() + self.rsd_client.get_node_url_by_uuid.assert_not_called() + + def test_terminate_connection(self): + mock_connector = {'system uuid': + "281bbc50-e76f-40e7-a757-06b916a83d6f"} + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.rsd_client.get_node_url_by_uuid = mock.Mock( + return_value=self.resource_url) + + self.volume.driver.terminate_connection(self.mock_volume, + mock_connector) + + self.rsd_client.get_node_url_by_uuid.assert_called_once() + self.rsd_client.detach_volume_from_node.assert_called_once() + + def test_terminate_connection_no_node(self): + mock_connector = {'system uuid': + "281bbc50-e76f-40e7-a757-06b916a83d6f"} + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.rsd_client.get_node_url_by_uuid = mock.Mock( + return_value="") + + self.assertRaises(exception.VolumeBackendAPIException, + self.volume.driver.terminate_connection, + self.mock_volume, mock_connector) + self.rsd_client.get_node_url_by_uuid.assert_called_once() + self.rsd_client.detach_volume_from_node.assert_not_called() + + def test_terminate_connection_no_connector(self): + mock_connector = None + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.rsd_client.get_node_url_by_uuid = mock.Mock( + return_value=self.resource_url) + + self.volume.driver.terminate_connection( + self.mock_volume, mock_connector) + self.rsd_client.detach_all_node_connections_for_volume. \ + assert_called_once() + self.rsd_client.get_node_url_by_uuid.assert_not_called() + self.rsd_client.detach_volume_from_node.assert_not_called() + + def test_terminate_connection_no_system_uuid(self): + mock_connector = {} + self.mock_dict['provider_location'] = self.resource_url + self.mock_volume.__getitem__.side_effect = self.mock_dict.__getitem__ + self.rsd_client.get_node_url_by_uuid = mock.Mock( + return_value=self.resource_url) + + self.assertRaises(exception.VolumeBackendAPIException, + self.volume.driver.terminate_connection, + self.mock_volume, mock_connector) + self.rsd_client.get_node_url_by_uuid.assert_not_called() + self.rsd_client.detach_volume_from_node.assert_not_called() + + def test_create_volume_from_snapshot(self): + mock_snap = mock.Mock() + mock_snap.provider_location = self.resource_url + mock_snap.volume_size = 10 + self.mock_volume.size = 10 + self.rsd_client.create_volume_from_snap = mock.Mock( + return_value=self.resource_url) + self.rsd_client.delete_vol_or_snap = mock.Mock() + + ret_dict = self.volume.driver.create_volume_from_snapshot( + self.mock_volume, mock_snap) + + self.assertEqual({'provider_location': self.resource_url}, ret_dict) + self.rsd_client.create_volume_from_snap.assert_called_once() + self.rsd_client.extend_volume.assert_not_called() + self.rsd_client.delete_vol_or_snap.assert_not_called() + + def test_create_volume_from_snapshot_diff_size(self): + mock_snap = mock.Mock() + mock_snap.provider_location = self.resource_url + mock_snap.volume_size = 10 + self.mock_volume.size = 20 + self.rsd_client.create_volume_from_snap = mock.Mock( + return_value=self.resource_url) + self.rsd_client.extend_volume = mock.Mock() + self.rsd_client.delete_vol_or_snap = mock.Mock() + + ret_dict = self.volume.driver.create_volume_from_snapshot( + self.mock_volume, mock_snap) + + self.assertEqual({'provider_location': self.resource_url}, ret_dict) + self.rsd_client.create_volume_from_snap.assert_called_once() + self.rsd_client.extend_volume.assert_called_once() + self.rsd_client.delete_vol_or_snap.assert_not_called() + + def test_create_volume_from_snapshot_diff_size_fail(self): + mock_snap = mock.Mock() + mock_snap.provider_location = self.resource_url + mock_snap.volume_size = 10 + self.mock_volume.size = 20 + self.rsd_client.create_volume_from_snap = mock.Mock( + return_value=self.resource_url) + self.rsd_client.extend_volume = mock.Mock( + side_effect=[exception.VolumeBackendAPIException( + data="extend fail")]) + self.rsd_client.delete_vol_or_snap = mock.Mock() + + self.assertRaises(exception.VolumeBackendAPIException, + self.volume.driver.create_volume_from_snapshot, + self.mock_volume, mock_snap) + + self.rsd_client.create_volume_from_snap.assert_called_once() + self.rsd_client.extend_volume.assert_called_once() + self.rsd_client.delete_vol_or_snap.assert_called_once() + + def test_delete_snapshot(self): + mock_snap = mock.Mock() + mock_snap.provider_location = self.resource_url + mock_snap.name = "mock_snapshot" + self.rsd_client.delete_vol_or_snap = mock.Mock(return_value=True) + + self.volume.driver.delete_snapshot(mock_snap) + + self.rsd_client.delete_vol_or_snap.assert_called_once() + + def test_delete_snapshot_no_url(self): + mock_snap = mock.Mock() + mock_snap.provider_location = "" + mock_snap.name = "mock_snapshot" + self.rsd_client.delete_vol_or_snap = mock.Mock(return_value=True) + + self.volume.driver.delete_snapshot(mock_snap) + + self.rsd_client.delete_vol_or_snap.assert_not_called() + + def test_delete_snapshot_unable_to_delete(self): + mock_snap = mock.Mock() + mock_snap.provider_location = self.resource_url + mock_snap.name = "mock_snapshot" + self.rsd_client.delete_vol_or_snap = mock.Mock( + side_effect=[exception.SnapshotIsBusy( + snapshot_name=mock_snap.name)]) + + self.assertRaises(exception.SnapshotIsBusy, + self.volume.driver.delete_snapshot, mock_snap) + + self.rsd_client.delete_vol_or_snap.assert_called_once() + + def test_create_cloned_volume(self): + mock_vref = mock.Mock() + mock_vref.provider_location = self.resource_url + mock_vref.size = 10 + self.mock_volume.size = 10 + self.rsd_client.clone_volume = mock.Mock( + return_value=(self.resource_url, self.resource_url)) + self.rsd_client.extend_volume = mock.Mock() + self.rsd_client.delete_vol_or_snap = mock.Mock() + + self.volume.driver.create_cloned_volume(self.mock_volume, mock_vref) + + self.rsd_client.clone_volume.assert_called_once() + self.rsd_client.extend_volume.assert_not_called() + self.rsd_client.delete_vol_or_snap.assert_not_called() + + def test_create_cloned_volume_extend_vol(self): + mock_vref = mock.Mock() + mock_vref.provider_location = self.resource_url + mock_vref.size = 20 + self.mock_volume.size = 10 + self.rsd_client.clone_volume = mock.Mock( + return_value=(self.resource_url, self.resource_url)) + self.rsd_client.extend_volume = mock.Mock() + self.rsd_client.delete_vol_or_snap = mock.Mock() + + self.volume.driver.create_cloned_volume(self.mock_volume, mock_vref) + + self.rsd_client.clone_volume.assert_called_once() + self.rsd_client.extend_volume.assert_called_once() + self.rsd_client.delete_vol_or_snap.assert_not_called() + + def test_create_cloned_volume_extend_vol_fail(self): + mock_vref = mock.Mock() + mock_vref.provider_location = self.resource_url + mock_vref.size = 20 + self.mock_volume.size = 10 + self.rsd_client.clone_volume = mock.Mock( + return_value=(self.resource_url, self.resource_url)) + self.rsd_client.extend_volume = mock.Mock( + side_effect=exception.VolumeBackendAPIException( + data="extend fail")) + self.rsd_client.delete_vol_or_snap = mock.Mock() + + self.assertRaises(exception.VolumeBackendAPIException, + self.volume.driver.create_cloned_volume, + self.mock_volume, mock_vref) + + self.rsd_client.clone_volume.assert_called_once() + self.rsd_client.extend_volume.assert_called_once() + self.assertEqual(2, self.rsd_client.delete_vol_or_snap.call_count) diff --git a/cinder/volume/drivers/rsd.py b/cinder/volume/drivers/rsd.py new file mode 100644 index 00000000000..4810471aa06 --- /dev/null +++ b/cinder/volume/drivers/rsd.py @@ -0,0 +1,723 @@ +# 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. + +""" +Driver for RackScale Design. + +""" + +import json + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units + +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder import utils +from cinder.volume import driver + +from distutils import version + +try: + from rsd_lib import RSDLib + from sushy import exceptions as sushy_exceptions +except ImportError: + # Used for tests, when no rsd-lib is installed + RSDLib = None + sushy_exceptions = None + +LOG = logging.getLogger(__name__) + +RSD_OPTS = [ + cfg.StrOpt('podm_url', + default='', + help='URL of PODM service'), + cfg.StrOpt('podm_username', + default='', + help='Username of PODM service'), + cfg.StrOpt('podm_password', + default='', + help='Password of PODM service', + secret=True), +] + + +class RSDRetryableException(exception.VolumeDriverException): + message = _("RSD retryable exception: %(reason)s") + + +def get_volume_metadata(volume): + metadata = volume.get('volume_metadata') + if metadata: + ret = {data['key']: data['value'] for data in metadata} + else: + ret = volume.get('metadata', {}) + return ret + + +class RSDClient(object): + def __init__(self, rsdlib): + self.rsdlib = rsdlib + + @classmethod + def initialize(cls, url, username, password, verify): + if not RSDLib: + raise exception.VolumeBackendAPIException( + data=(_("RSDLib is not available, please install rsd-lib."))) + + try: + rsdlib = RSDLib(url, username, password, verify=verify).factory() + except Exception: + # error credentials may throw unexpected exception + LOG.exception("Cannot connect to RSD PODM") + raise exception.VolumeBackendAPIException( + data=_("initialize: Cannot connect to RSD PODM.")) + + rsd_lib_version = version.LooseVersion(rsdlib._rsd_api_version) + if rsd_lib_version < version.LooseVersion("2.4.0"): + raise exception.VolumeBackendAPIException( + data=(_("initialize: Unsupported rsd_lib version: " + "%(current)s < %(expected)s.") + % {'current': rsdlib._rsd_api_version, + 'expected': "2.4.0"})) + + LOG.info("initialize: Connected to %s at version %s.", + url, rsdlib._rsd_api_version) + return cls(rsdlib) + + def _get_storage(self, storage_url): + ss_url = "/".join(storage_url.split("/", 5)[:5]) + storage_service = self.rsdlib.get_storage_service(ss_url) + return storage_service + + def _get_storages(self, filter_nvme=True): + ret = [] + for storage in (self.rsdlib + .get_storage_service_collection().get_members()): + if filter_nvme: + drives = storage.drives.get_members() + if drives and (any(map(lambda drive: + False if not drive.protocol + else 'nvme' in drive.protocol.lower(), + drives))): + ret.append(storage) + else: + ret.append(storage) + return ret + + def _get_node(self, node_url): + return self.rsdlib.get_node(node_url) + + def _get_volume(self, volume_url): + ss = self._get_storage(volume_url) + volume = ss.volumes.get_member(volume_url) + return volume + + def _get_providing_pool(self, volume): + len_cs = len(volume.capacity_sources) + if len_cs != 1: + raise exception.ValidationError( + detail=(_("Volume %(vol)s has %(len_cs)d capacity_sources!") + % {'vol': volume.path, + 'len_cs': len_cs})) + len_pp = len(volume.capacity_sources[0].providing_pools) + if len_pp != 1: + raise exception.ValidationError( + detail=(_("Volume %(vol)s has %(len_pp)d providing_pools!") + % {'vol': volume.path, + 'len_pp': len_pp})) + return volume.capacity_sources[0].providing_pools[0] + + def _create_vol_or_snap(self, + storage, + size_in_bytes, + pool_url=None, + source_snap=None, + source_vol=None): + capacity_sources = None + if pool_url: + capacity_sources = [{ + "ProvidingPools": [{ + "@odata.id": pool_url + }] + }] + + replica_infos = None + if source_snap: + replica_infos = [{ + "ReplicaType": "Clone", + "Replica": {"@odata.id": source_snap} + }] + if source_vol: + raise exception.InvalidInput( + reason=(_("Cannot specify both source_snap=%(snap)s and " + "source_vol=%(vol)s!") + % {'snap': source_snap, + 'vol': source_vol})) + elif source_vol: + replica_infos = [{ + "ReplicaType": "Snapshot", + "Replica": {"@odata.id": source_vol} + }] + + LOG.debug("Creating... with size_byte=%s, " + "capacity_sources=%s, replica_infos=%s", + size_in_bytes, capacity_sources, replica_infos) + volume_url = storage.volumes.create_volume( + size_in_bytes, + capacity_sources=capacity_sources, + replica_infos=replica_infos) + LOG.debug("Created volume_url=%s", volume_url) + return volume_url + + def create_volume(self, size_in_gb): + size_in_bytes = size_in_gb * units.Gi + try: + for storage in self._get_storages(): + try: + volume_url = self._create_vol_or_snap( + storage, size_in_bytes) + LOG.info("RSD volume %s created, with size %s GiB", + volume_url, size_in_gb) + return volume_url + # NOTE(Yingxin): Currently, we capture sushy_exception to + # identify that volume creation is failed at RSD backend. + except (sushy_exceptions.HTTPError, + sushy_exceptions.ConnectionError) as e: + LOG.warning("skipped storage %s for creation error %s", + storage.path, e) + except Exception: + LOG.exception("Create volume failed") + + raise exception.VolumeBackendAPIException( + data=(_('Unable to create new volume with %d GiB') % size_in_gb)) + + def create_snap(self, volume_url): + try: + ss = self._get_storage(volume_url) + volume = self._get_volume(volume_url) + pool_url = self._get_providing_pool(volume) + snap_url = self._create_vol_or_snap( + ss, volume.capacity_bytes, + pool_url=pool_url, + source_vol=volume_url) + LOG.info("RSD snapshot %s created, from volume %s", + snap_url, volume_url) + return snap_url + except Exception: + LOG.exception("Create snapshot failed") + raise exception.VolumeBackendAPIException( + data=(_('Unable to create snapshot from volume %s') + % volume_url)) + + def create_volume_from_snap(self, snap_url, size_in_gb=None): + try: + ss = self._get_storage(snap_url) + snap = self._get_volume(snap_url) + if not size_in_gb: + size_in_bytes = snap.capacity_bytes + else: + size_in_bytes = size_in_gb * units.Gi + pool_url = self._get_providing_pool(snap) + volume_url = self._create_vol_or_snap( + ss, size_in_bytes, + pool_url=pool_url, + source_snap=snap_url) + LOG.info("RSD volume %s created, from snap %s, " + "with size %s GiB.", + volume_url, snap_url, + size_in_bytes / units.Gi) + return volume_url + except Exception: + LOG.exception("Create volume from snapshot failed") + raise exception.VolumeBackendAPIException( + data=(_('Unable to create volume from snapshot %s') + % snap_url)) + + def clone_volume(self, volume_url, size_in_gb=None): + try: + ss = self._get_storage(volume_url) + origin_volume = self._get_volume(volume_url) + pool_url = self._get_providing_pool(origin_volume) + snap_url = self._create_vol_or_snap( + ss, origin_volume.capacity_bytes, + pool_url=pool_url, + source_vol=volume_url) + except Exception: + LOG.exception("Clone volume failed (create snapshot phase)") + raise exception.VolumeBackendAPIException( + data=(_('Unable to create volume from volume %s, snapshot ' + 'creation failed.') + % volume_url)) + try: + if not size_in_gb: + size_in_bytes = origin_volume.capacity_bytes + else: + size_in_bytes = size_in_gb * units.Gi + new_vol_url = self._create_vol_or_snap( + ss, size_in_bytes, + pool_url=pool_url, + source_snap=snap_url) + LOG.info("RSD volume %s created, from volume %s and snap %s, " + "with size %s GiB.", + new_vol_url, volume_url, snap_url, + size_in_bytes / units.Gi) + return new_vol_url, snap_url + except Exception: + LOG.exception("Clone volume failed (clone volume phase)") + try: + self.delete_vol_or_snap(snap_url) + except Exception: + LOG.exception("Clone volume failed (undo snapshot)") + raise exception.VolumeBackendAPIException( + data=(_('Unable to delete the temp snapshot %(snap)s, ' + 'during a failure to clone volume %(vol)s.') + % {'snap': snap_url, + 'vol': volume_url})) + raise exception.VolumeBackendAPIException( + data=(_('Unable to create volume from volume %s, volume ' + 'creation failed.') + % volume_url)) + + def extend_volume(self, volume_url, size_in_gb): + size_in_bytes = size_in_gb * units.Gi + try: + volume = self._get_volume(volume_url) + volume.resize(size_in_bytes) + LOG.info("RSD volume %s resized to %s Bytes", + volume.path, size_in_bytes) + except Exception: + LOG.exception("Extend volume failed") + raise exception.VolumeBackendAPIException( + data=(_('Unable to extend volume %s.') % volume_url)) + + def delete_vol_or_snap(self, volume_url, + volume_name='', ignore_non_exist=False): + try: + try: + volume = self._get_volume(volume_url) + except sushy_exceptions.ResourceNotFoundError: + if ignore_non_exist: + LOG.warning("Deleted non existent vol/snap %s", volume_url) + else: + raise + if volume.links.endpoints: + LOG.warning("Delete vol/snap failed, attached: %s", volume_url) + raise exception.VolumeIsBusy(_("Volume is already attached"), + volume_name=volume_name) + volume.delete() + except sushy_exceptions.BadRequestError as e: + try: + msg = e.body['@Message.ExtendedInfo'][0]['Message'] + if (msg == "Cannot delete source snapshot volume when " + "other clone volumes are based on this snapshot."): + LOG.warning("Delete vol/snap failed, has-deps: %s", + volume_url) + raise exception.SnapshotIsBusy(snapshot_name=volume_name) + except Exception: + LOG.exception("Delete vol/snap failed") + raise exception.VolumeBackendAPIException( + data=(_('Unable to delete volume %s.') % volume_url)) + except Exception: + LOG.exception("Delete vol/snap failed") + raise exception.VolumeBackendAPIException( + data=(_('Unable to delete volume %s.') % volume_url)) + LOG.info("RSD volume deleted: %s", volume_url) + + def get_node_url_by_uuid(self, uuid): + uuid = uuid.upper() + try: + nodes = self.rsdlib.get_node_collection().get_members() + for node in nodes: + node_system = None + if node: + node_system = self.rsdlib.get_system( + node.links.computer_system) + if (node and + node_system and + node_system.uuid and + node_system.uuid.upper() == uuid): + return node.path + except Exception: + LOG.exception("Get node url failed") + return "" + + def get_stats(self): + free_capacity_gb = 0 + total_capacity_gb = 0 + allocated_capacity_gb = 0 + total_volumes = 0 + try: + storages = self._get_storages() + for storage in storages: + for pool in storage.storage_pools.get_members(): + total_capacity_gb += ( + float(pool.capacity.allocated_bytes or 0) / units.Gi) + allocated_capacity_gb += ( + float(pool.capacity.consumed_bytes or 0) / units.Gi) + total_volumes += len(storage.volumes.members_identities) + free_capacity_gb = total_capacity_gb - allocated_capacity_gb + LOG.info("Got RSD stats: free_gb:%s, total_gb:%s, " + "allocated_gb:%s, volumes:%s", + free_capacity_gb, + total_capacity_gb, + allocated_capacity_gb, + total_volumes) + except Exception: + LOG.exception("Get stats failed") + + return (free_capacity_gb, + total_capacity_gb, + allocated_capacity_gb, + total_volumes) + + def _get_nqn_endpoints(self, endpoint_urls): + ret = [] + for endpoint_url in endpoint_urls: + endpoint_json = ( + json.loads(self.rsdlib._conn.get(endpoint_url).text)) + for ident in endpoint_json["Identifiers"]: + if ident["DurableNameFormat"] == "NQN": + nqn = ident["DurableName"] + ret.append((nqn, endpoint_json)) + break + return ret + + @utils.retry(RSDRetryableException, + interval=4, + retries=5, + backoff_rate=2) + def attach_volume_to_node(self, volume_url, node_url): + LOG.info('Trying attach from node %s to volume %s', + node_url, volume_url) + try: + volume = self._get_volume(volume_url) + node = self._get_node(node_url) + if len(volume.links.endpoints) > 0: + raise exception.ValidationError( + detail=(_("Volume %s already attached") % volume_url)) + + node.attach_endpoint(volume.path) + except sushy_exceptions.InvalidParameterValueError as e: + LOG.exception("Attach volume failed (not allowable)") + raise RSDRetryableException( + reason=(_("Not allowed to attach from " + "%(node)s to %(volume)s.") + % {'node': node_url, + 'volume': volume_url})) + except Exception: + LOG.exception("Attach volume failed (attach phase)") + raise exception.VolumeBackendAPIException( + data=(_("Attach failed from %(node)s to %(volume)s.") + % {'node': node_url, + 'volume': volume_url})) + try: + volume.refresh() + node.refresh() + + v_endpoints = volume.links.endpoints + v_endpoints = self._get_nqn_endpoints(v_endpoints) + if len(v_endpoints) != 1: + raise exception.ValidationError( + detail=(_("Attach volume error: %d target nqns") + % len(v_endpoints))) + target_nqn, v_endpoint = v_endpoints[0] + ip_transports = v_endpoint["IPTransportDetails"] + if len(ip_transports) != 1: + raise exception.ValidationError( + detail=(_("Attach volume error: %d target ips") + % len(ip_transports))) + ip_transport = ip_transports[0] + target_ip = ip_transport["IPv4Address"]["Address"] + target_port = ip_transport["Port"] + + node_system = self.rsdlib.get_system(node.links.computer_system) + n_endpoints = tuple( + val["@odata.id"] + for val in node_system.json["Links"]["Endpoints"]) + n_endpoints = self._get_nqn_endpoints(n_endpoints) + if len(n_endpoints) == 0: + raise exception.ValidationError( + detail=(_("Attach volume error: %d host nqns") + % len(n_endpoints))) + host_nqn, v_endpoint = n_endpoints[0] + + LOG.info('Attachment successful: Retrieved target IP %s, ' + 'target Port %s, target NQN %s and initiator NQN %s', + target_ip, target_port, target_nqn, host_nqn) + return (target_ip, target_port, target_nqn, host_nqn) + except Exception as e: + LOG.exception("Attach volume failed (post-attach)") + try: + node.refresh() + node.detach_endpoint(volume.path) + LOG.info('Detached from node %s to volume %s', + node_url, volume_url) + except Exception: + LOG.exception("Attach volume failed (undo attach)") + raise exception.VolumeBackendAPIException( + data=(_("Undo-attach failed from %(node)s to %(volume)s.") + % {'node': node_url, + 'volume': volume_url})) + if isinstance(e, exception.ValidationError): + raise RSDRetryableException( + reason=(_("Validation error during post-attach from " + "%(node)s to %(volume)s.") + % {'node': node_url, + 'volume': volume_url})) + else: + raise exception.VolumeBackendAPIException( + data=(_("Post-attach failed from %(node)s to %(volume)s.") + % {'node': node_url, + 'volume': volume_url})) + + def detach_volume_from_node(self, volume_url, node_url): + LOG.info('Trying detach from node %s for volume %s', + node_url, volume_url) + try: + volume = self._get_volume(volume_url) + node = self._get_node(node_url) + node.detach_endpoint(volume.path) + except Exception: + LOG.exception("Detach volume failed") + raise exception.VolumeBackendAPIException( + data=(_("Detach failed from %(node)s for %(volume)s.") + % {'node': node_url, + 'volume': volume_url})) + + def detach_all_node_connections_for_volume(self, volume_url): + try: + volume = self._get_volume(volume_url) + nodes = self.rsdlib.get_node_collection().get_members() + for node in nodes: + if node: + if volume.path in node.get_allowed_detach_endpoints(): + node.detach_endpoint(volume.path) + except Exception: + LOG.exception("Detach failed for volume from all host " + "connections") + raise exception.VolumeBackendAPIException( + data=(_("Detach failed for %(volume)s from all host " + "connections.") + % {'volume': volume_url})) + + +@interface.volumedriver +class RSDDriver(driver.VolumeDriver): + """Openstack driver to perform NVMe-oF volume management in RSD Solution + + .. code-block:: none + + Version History: + 1.0.0: Initial driver + """ + + VERSION = '1.0.0' + CI_WIKI_NAME = 'INTEL-RSD-CI' + + def __init__(self, *args, **kwargs): + super(RSDDriver, self).__init__(*args, **kwargs) + + self.configuration.append_config_values(RSD_OPTS) + self.rsdClient = None + + @staticmethod + def get_driver_options(): + return RSD_OPTS + + @utils.trace + def do_setup(self, context): + self.rsdClient = RSDClient.initialize( + self.configuration.podm_url, + self.configuration.podm_username, + self.configuration.podm_password, + self.configuration.suppress_requests_ssl_warnings) + + def check_for_setup_error(self): + pass + + @utils.trace + def create_volume(self, volume): + size_in_gb = int(volume['size']) + volume_url = self.rsdClient.create_volume(size_in_gb) + return {'provider_location': volume_url} + + @utils.trace + def delete_volume(self, volume): + volume_url = volume['provider_location'] + if not volume_url: + return + self.rsdClient.delete_vol_or_snap(volume_url, + volume_name=volume.name, + ignore_non_exist=True) + provider_snap_url = volume.metadata.get("rsd_provider_snap") + if provider_snap_url: + self.rsdClient.delete_vol_or_snap(provider_snap_url, + volume_name=volume.name, + ignore_non_exist=True) + + def _update_volume_stats(self): + backend_name = ( + self.configuration.safe_get('volume_backend_name') or 'RSD') + + ret = self.rsdClient.get_stats() + (free_capacity_gb, + total_capacity_gb, + allocated_capacity_gb, + total_volumes) = ret + + spool = {} + spool['pool_name'] = backend_name + spool['total_capacity_gb'] = total_capacity_gb + spool['free_capacity_gb'] = free_capacity_gb + spool['allocated_capacity_gb'] = allocated_capacity_gb + spool['thin_provisioning_support'] = True + spool['thick_provisioning_support'] = True + spool['multiattach'] = False + + self._stats['volume_backend_name'] = backend_name + self._stats['vendor_name'] = 'Intel' + self._stats['driver_version'] = self.VERSION + self._stats['storage_protocol'] = 'nvmeof' + # SinglePool + self._stats['pools'] = [spool] + + @utils.trace + def get_volume_stats(self, refresh=False): + if refresh: + self._update_volume_stats() + return self._stats + + @utils.trace + def initialize_connection(self, volume, connector, **kwargs): + uuid = connector.get("system uuid") + if not uuid: + msg = _("initialize_connection error: no uuid available!") + LOG.exception(msg) + raise exception.VolumeBackendAPIException(msg) + node_url = self.rsdClient.get_node_url_by_uuid(uuid) + if not node_url: + msg = (_("initialize_connection error: no node_url from uuid %s!") + % uuid) + LOG.exception(msg) + raise exception.VolumeBackendAPIException(msg) + + volume_url = volume['provider_location'] + target_ip, target_port, target_nqn, initiator_nqn = ( + self.rsdClient.attach_volume_to_node(volume_url, node_url)) + conn_info = { + 'driver_volume_type': 'nvmeof', + 'data': { + 'transport_type': 'rdma', + 'host_nqn': initiator_nqn, + 'nqn': target_nqn, + 'target_port': target_port, + 'target_portal': target_ip, + } + } + return conn_info + + @utils.trace + def terminate_connection(self, volume, connector, **kwargs): + if connector is None: + # None connector means force-detach + volume_url = volume['provider_location'] + self.rsdClient.detach_all_node_connections_for_volume(volume_url) + return + + uuid = connector.get("system uuid") + if not uuid: + msg = _("terminate_connection error: no uuid available!") + LOG.exception(msg) + raise exception.VolumeBackendAPIException(msg) + node_url = self.rsdClient.get_node_url_by_uuid(uuid) + if not node_url: + msg = (_("terminate_connection error: no node_url from uuid %s!") + % uuid) + LOG.exception(msg) + raise exception.VolumeBackendAPIException(msg) + + volume_url = volume['provider_location'] + self.rsdClient.detach_volume_from_node(volume_url, node_url) + + def ensure_export(self, context, volume): + pass + + def create_export(self, context, volume, connector): + pass + + def remove_export(self, context, volume): + pass + + @utils.trace + def create_volume_from_snapshot(self, volume, snapshot): + snap_url = snapshot.provider_location + old_size_in_gb = snapshot.volume_size + size_in_gb = volume.size + volume_url = self.rsdClient.create_volume_from_snap(snap_url) + if size_in_gb != old_size_in_gb: + try: + self.rsdClient.extend_volume(volume_url, size_in_gb) + except Exception: + self.rsdClient.delete_vol_or_snap(volume_url, + volume_name=volume.name) + raise + return {'provider_location': volume_url} + + @utils.trace + def create_snapshot(self, snapshot): + volume_url = snapshot.volume.provider_location + snap_url = self.rsdClient.create_snap(volume_url) + snapshot.provider_location = snap_url + snapshot.save() + + @utils.trace + def delete_snapshot(self, snapshot): + snap_url = snapshot.provider_location + if not snap_url: + return + self.rsdClient.delete_vol_or_snap(snap_url, + volume_name=snapshot.name, + ignore_non_exist=True) + + @utils.trace + def extend_volume(self, volume, new_size): + volume_url = volume.provider_location + self.rsdClient.extend_volume(volume_url, new_size) + + def clone_image(self, context, volume, + image_location, image_meta, + image_service): + return None, False + + @utils.trace + def create_cloned_volume(self, volume, src_vref): + volume_url = src_vref.provider_location + old_size_in_gb = src_vref.size + size_in_gb = volume.size + new_vol_url, provider_snap_url = self.rsdClient.clone_volume( + volume_url) + metadata = get_volume_metadata(volume) + metadata["rsd_provider_snap"] = provider_snap_url + if size_in_gb != old_size_in_gb: + try: + self.rsdClient.extend_volume(new_vol_url, size_in_gb) + except Exception: + self.rsdClient.delete_vol_or_snap(new_vol_url, + volume_name=volume.name) + self.rsdClient.delete_vol_or_snap(provider_snap_url, + volume_name=volume.name) + raise + return {'provider_location': new_vol_url, + 'metadata': metadata} diff --git a/doc/source/configuration/block-storage/drivers/rsd-volume-driver.rst b/doc/source/configuration/block-storage/drivers/rsd-volume-driver.rst new file mode 100644 index 00000000000..2dad2861ecc --- /dev/null +++ b/doc/source/configuration/block-storage/drivers/rsd-volume-driver.rst @@ -0,0 +1,52 @@ +==================================== +Intel Rack Scale Design (RSD) driver +==================================== + +The Intel Rack Scale Design volume driver is a block storage driver providing +NVMe-oF support for RSD storage. + +System requirements +~~~~~~~~~~~~~~~~~~~ + +To use the RSD driver, the following requirements are needed: + +* The driver only supports RSD API at version 2.4 or later. +* The driver requires rsd-lib. +* ``cinder-volume`` should be running on one of the composed node in RSD, and + have access to the PODM url. +* All the ``nova-compute`` services should be running on the composed nodes in + RSD. +* All the ``cinder-volume`` and ``nova-compute`` nodes should have installed + ``dmidecode`` and the latest ``nvme-cli`` with connect/disconnect + subcommands. + +Supported operations +~~~~~~~~~~~~~~~~~~~~ + +* Create, delete volumes. +* Attach, detach volumes. +* Copy an image to a volume. +* Copy a volume to an image. +* Create, delete snapshots. +* Create a volume from a snapshot. +* Clone a volume. +* Extend a volume. +* Get volume statistics. + +Configuration +~~~~~~~~~~~~~ + +On ``cinder-volume`` nodes, using the following configurations in your +``/etc/cinder/cinder.conf``: + +.. code-block:: ini + + volume_driver = cinder.volume.drivers.rsd.RSDDriver + +The following table contains the configuration options supported by the +RSD driver: + +.. config-table:: + :config-target: RSD + + cinder.volume.drivers.rsd diff --git a/driver-requirements.txt b/driver-requirements.txt index c05bd93a717..def3a9a8e51 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -44,3 +44,6 @@ infi.dtypes.iqn # PSF # Storpool storpool # Apache-2.0 + +# RSD Driver +rsd-lib # Apache-2.0 diff --git a/releasenotes/notes/rsd-cinder-driver-d71b88292536bfea.yaml b/releasenotes/notes/rsd-cinder-driver-d71b88292536bfea.yaml new file mode 100644 index 00000000000..95ef0122f03 --- /dev/null +++ b/releasenotes/notes/rsd-cinder-driver-d71b88292536bfea.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Added a new Cinder driver for RackScale Design NVMe-oF storage solution.