From c9eada86f29406a72c9d5d5a8b123c65ae69a4b9 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Fri, 9 Dec 2016 12:11:24 -0800 Subject: [PATCH] Add Rest API Support for Nimble Storage Rest API Support for all the basic volume driver features supported by the Nimble Storage DocImpact Implements: blueprint nimble-rest-api-support Change-Id: I8f8a79b904c3b4c298a1299f5b463ec8c0b7e7a8 --- .../tests/unit/volume/drivers/test_nimble.py | 974 +++++------ cinder/volume/drivers/nimble.py | 1466 ++++++++++------- ...ble-rest-api-support-75c2324ee462d026.yaml | 4 + 3 files changed, 1283 insertions(+), 1161 deletions(-) create mode 100644 releasenotes/notes/nimble-rest-api-support-75c2324ee462d026.yaml diff --git a/cinder/tests/unit/volume/drivers/test_nimble.py b/cinder/tests/unit/volume/drivers/test_nimble.py index 245f65cc0eb..b7c4be67829 100644 --- a/cinder/tests/unit/volume/drivers/test_nimble.py +++ b/cinder/tests/unit/volume/drivers/test_nimble.py @@ -13,10 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -import sys import mock -from oslo_utils import units +import sys from cinder import context from cinder import exception @@ -26,147 +25,144 @@ from cinder.tests.unit import fake_constants as fake from cinder.volume.drivers import nimble from cinder.volume import volume_types - -NIMBLE_CLIENT = 'cinder.volume.drivers.nimble.client' -NIMBLE_URLLIB2 = 'six.moves.urllib.request' +NIMBLE_CLIENT = 'cinder.volume.drivers.nimble.NimbleRestAPIExecutor' +NIMBLE_URLLIB2 = 'cinder.volume.drivers.nimble.requests' NIMBLE_RANDOM = 'cinder.volume.drivers.nimble.random' NIMBLE_ISCSI_DRIVER = 'cinder.volume.drivers.nimble.NimbleISCSIDriver' -NIMBLE_BASE_DRIVER = 'cinder.volume.drivers.nimble.NimbleBaseVolumeDriver' NIMBLE_FC_DRIVER = 'cinder.volume.drivers.nimble.NimbleFCDriver' -DRIVER_VERSION = '3.1.0' +DRIVER_VERSION = '4.0.0' +nimble.DEFAULT_SLEEP = 0 -FAKE_ENUM_STRING = """ - - - - - - - - - """ +FAKE_POSITIVE_LOGIN_RESPONSE_1 = '2c20aad78a220ed1dae21dcd6f9446f5' -FAKE_POSITIVE_LOGIN_RESPONSE_1 = {'err-list': {'err-list': - [{'code': 0}]}, - 'authInfo': {'sid': "a9b9aba7"}} +FAKE_POSITIVE_LOGIN_RESPONSE_2 = '2c20aad78a220ed1dae21dcd6f9446ff' -FAKE_POSITIVE_LOGIN_RESPONSE_2 = {'err-list': {'err-list': - [{'code': 0}]}, - 'authInfo': {'sid': "a9f3eba7"}} +FAKE_POSITIVE_HEADERS = {'X-Auth-Token': FAKE_POSITIVE_LOGIN_RESPONSE_1} FAKE_POSITIVE_NETCONFIG_RESPONSE = { - 'config': {'subnet-list': [{'label': "data1", - 'subnet-id': {'type': 3}, - 'discovery-ip': "172.18.108.21"}, - {'label': "mgmt-data", - 'subnet-id': - {'type': 4}, - 'discovery-ip': "10.18.108.55"}]}, - 'err-list': {'err-list': [{'code': 0}]}} + 'role': 'active', + 'subnet_list': [{'network': '172.18.212.0', + 'discovery_ip': '172.18.108.21', + 'type': 'data', + 'allow_iscsi': True, + 'label': 'data1', + 'allow_group': True, + 'vlan_id': 0}], + 'array_list': [{'nic_list': [{'subnet_label': 'data1', + 'tagged': False, + 'data_ip': '172.18.212.82', + 'name': 'eth3'}]}], + 'name': 'test-array'} -FAKE_NEGATIVE_NETCONFIG_RESPONSE = {'err-list': {'err-list': - [{'code': 13}]}} +FAKE_NEGATIVE_NETCONFIG_RESPONSE = exception.VolumeDriverException( + "Session expired") -FAKE_CREATE_VOLUME_POSITIVE_RESPONSE = {'err-list': {'err-list': - [{'code': 0}]}, - 'name': "openstack-test11"} +FAKE_CREATE_VOLUME_POSITIVE_RESPONSE = { + 'clone': False, + 'name': "testvolume"} -FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_ENCRYPTION = {'err-list': {'err-list': - [{'code': 0}]}, - 'name': - "openstack-test-encryption"} +FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_ENCRYPTION = { + 'clone': False, + 'name': "testvolume-encryption"} -FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_PERFPOLICY = {'err-list': {'err-list': - [{'code': 0}]}, - 'name': - "openstack-test-perfpolicy"} +FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_PERF_POLICY = { + 'clone': False, + 'name': "testvolume-perf-policy"} -FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE = {'err-list': {'err-list': - [{'code': 17}]}, - 'name': "openstack-test11"} +FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_MULTI_INITIATOR = { + 'clone': False, + 'name': "testvolume-multi-initiator"} -FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE_ENCRYPTION = {'err-list': {'err-list': - [{'code': 17}]}, - 'name': - "openstack-test-encryption"} +FAKE_GET_VOL_INFO_RESPONSE = {'name': 'testvolume', + 'clone': False, + 'target_name': 'iqn.test', + 'online': True, + 'agent_type': 'openstack'} -FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE_PERFPOLICY = {'err-list': {'err-list': - [{'code': 17}]}, - 'name': - "openstack-test-perfpolicy"} +FAKE_GET_VOL_INFO_RESPONSE_MANAGE = {'name': 'testvolume', + 'agent_type': 'none', + 'online': False, + 'target_name': 'iqn.test'} -FAKE_GENERIC_POSITIVE_RESPONSE = {'err-list': {'err-list': - [{'code': 0}]}} +FAKE_GET_VOL_INFO_ONLINE = {'name': 'testvolume', + 'size': 2048, + 'online': True, + 'agent_type': 'none'} -FAKE_POSITIVE_GROUP_CONFIG_RESPONSE = { - 'err-list': {'err-list': [{'code': 0}]}, - 'info': {'usableCapacity': 8016883089408, - 'volUsageCompressed': 2938311843, - 'snapUsageCompressed': 36189, - 'unusedReserve': 0, - 'spaceInfoValid': True}} - -FAKE_IGROUP_LIST_RESPONSE = { - 'err-list': {'err-list': [{'code': 0}]}, - 'initiatorgrp-list': [ - {'initiator-list': [{'name': 'test-initiator1'}, - {'name': 'test-initiator2'}], - 'name': 'test-igrp1'}, - {'initiator-list': [{'name': 'test-initiator1'}], - 'name': 'test-igrp2'}]} - -FAKE_IGROUP_LIST_RESPONSE_FC = { - 'err-list': {'err-list': [{'code': 0}]}, - 'initiatorgrp-list': [ - {'initiator-list': [{'wwpn': '10:00:00:00:00:00:00:00'}], - 'name': 'test-igrp2'}, - {'initiator-list': [{'wwpn': '10:00:00:00:00:00:00:00'}, - {'wwpn': '10:00:00:00:00:00:00:01'}], - 'name': 'test-igrp1'}]} - -FAKE_GET_VOL_INFO_RESPONSE = { - 'err-list': {'err-list': [{'code': 0}]}, - 'vol': {'target-name': 'iqn.test', - 'name': 'test_vol', - 'agent-type': 1, - 'online': False}} - -FAKE_GET_VOL_INFO_BACKUP_RESPONSE = { - 'err-list': {'err-list': [{'code': 0}]}, - 'vol': {'target-name': 'iqn.test', - 'name': 'test_vol', - 'agent-type': 1, - 'clone': 1, - 'base-snap': 'test-backup-snap', - 'parent-vol': 'volume-' + fake.VOLUME2_ID, - 'online': False}} +FAKE_GET_VOL_INFO_BACKUP_RESPONSE = {'name': 'testvolume', + 'clone': True, + 'target_name': 'iqn.test', + 'online': False, + 'agent_type': 'openstack', + 'parent_vol_id': 'volume-' + + fake.VOLUME2_ID, + 'base_snap_id': 'test-backup-snap'} FAKE_GET_SNAP_INFO_BACKUP_RESPONSE = { - 'err-list': {'err-list': [{'code': 0}]}, - 'snap': {'description': "backup-vol-" + fake.VOLUME2_ID, - 'name': 'test-backup-snap', - 'vol': 'volume-' + fake.VOLUME_ID} -} + 'description': "backup-vol-" + fake.VOLUME2_ID, + 'name': 'test-backup-snap', + 'id': fake.SNAPSHOT_ID, + 'vol_id': fake.VOLUME_ID, + 'volume_name': 'volume-' + fake.VOLUME_ID} -FAKE_GET_VOL_INFO_ONLINE = { - 'err-list': {'err-list': [{'code': 0}]}, - 'vol': {'target-name': 'iqn.test', - 'name': 'test_vol', - 'agent-type': 1, - 'size': int(1.75 * units.Gi), - 'online': True}} +FAKE_POSITIVE_GROUP_CONFIG_RESPONSE = { + 'name': 'group-test', + 'version_current': '0.0.0.0', + 'access_protocol_list': ['iscsi']} -FAKE_GET_VOL_INFO_ERROR = { - 'err-list': {'err-list': [{'code': 2}]}, - 'vol': {'target-name': 'iqn.test'}} +FAKE_LOGIN_POST_RESPONSE = { + 'data': {'session_token': FAKE_POSITIVE_LOGIN_RESPONSE_1}} -FAKE_GET_VOL_INFO_RESPONSE_WITH_SET_AGENT_TYPE = { - 'err-list': {'err-list': [{'code': 0}]}, - 'vol': {'target-name': 'iqn.test', - 'name': 'test_vol', - 'agent-type': 5}} +FAKE_EXTEND_VOLUME_PARAMS = {'data': {'size': 5120, + 'reserve': 0, + 'warn_level': 80, + 'limit': 100, + 'snap_limit': sys.maxsize}} + +FAKE_IGROUP_LIST_RESPONSE = [ + {'iscsi_initiators': [{'iqn': 'test-initiator1'}], + 'name': 'test-igrp1'}, + {'iscsi_initiators': [{'iqn': 'test-initiator2'}], + 'name': 'test-igrp2'}] + +FAKE_IGROUP_LIST_RESPONSE_FC = [ + {'fc_initiators': [{'wwpn': '10:00:00:00:00:00:00:00'}], + 'name': 'test-igrp1'}, + {'fc_initiators': [{'wwpn': '10:00:00:00:00:00:00:00'}, + {'wwpn': '10:00:00:00:00:00:00:01'}], + 'name': 'test-igrp2'}] + + +FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE = exception.VolumeBackendAPIException( + "Volume testvolume not found") + +FAKE_VOLUME_INFO_NEGATIVE_RESPONSE = exception.VolumeBackendAPIException( + "Volume testvolume not found") + +FAKE_CREATE_VOLUME_NEGATIVE_ENCRYPTION = exception.VolumeBackendAPIException( + "Volume testvolume-encryption not found") + +FAKE_CREATE_VOLUME_NEGATIVE_PERFPOLICY = exception.VolumeBackendAPIException( + "Volume testvolume-perfpolicy not found") + +FAKE_POSITIVE_GROUP_INFO_RESPONSE = { + 'version_current': '3.0.0.0', + 'group_target_enabled': False, + 'name': 'group-nimble', + 'usage_valid': True, + 'usable_capacity_bytes': 8016883089408, + 'compressed_vol_usage_bytes': 2938311843, + 'compressed_snap_usage_bytes': 36189, + 'unused_reserve_bytes': 0} + +FAKE_GENERIC_POSITIVE_RESPONSE = "" FAKE_TYPE_ID = fake.VOLUME_TYPE_ID +FAKE_POOL_ID = fake.GROUP_ID +FAKE_PERFORMANCE_POLICY_ID = fake.OBJECT_ID +NIMBLE_MANAGEMENT_IP = "10.18.108.55" +NIMBLE_SAN_LOGIN = "nimble" +NIMBLE_SAN_PASS = "nimble_pass" def create_configuration(username, password, ip_address, @@ -200,16 +196,16 @@ class NimbleDriverBaseTestCase(test.TestCase): self, mock_client_class, mock_urllib2, *args, **kwargs): self.mock_client_class = mock_client_class self.mock_client_service = mock.MagicMock(name='Client') - self.mock_client_class.Client.return_value = \ - self.mock_client_service - mock_wsdl = mock_urllib2.urlopen.return_value - mock_wsdl.read = mock.MagicMock() - mock_wsdl.read.return_value = FAKE_ENUM_STRING + self.mock_client_class.return_value = self.mock_client_service self.driver = nimble.NimbleISCSIDriver( configuration=configuration) - self.mock_client_service.service.login.return_value = \ - FAKE_POSITIVE_LOGIN_RESPONSE_1 + mock_login_response = mock_urllib2.post.return_value + mock_login_response = mock.MagicMock() + mock_login_response.status_code.return_value = 200 + mock_login_response.json.return_value = ( + FAKE_LOGIN_POST_RESPONSE) self.driver.do_setup(context.get_admin_context()) + self.driver.APIExecutor.login() func(self, *args, **kwargs) return inner_client_mock return client_mock_wrapper @@ -221,16 +217,17 @@ class NimbleDriverBaseTestCase(test.TestCase): self, mock_client_class, mock_urllib2, *args, **kwargs): self.mock_client_class = mock_client_class self.mock_client_service = mock.MagicMock(name='Client') - self.mock_client_class.Client.return_value = ( + self.mock_client_class.return_value = ( self.mock_client_service) - mock_wsdl = mock_urllib2.urlopen.return_value - mock_wsdl.read = mock.MagicMock() - mock_wsdl.read.return_value = FAKE_ENUM_STRING self.driver = nimble.NimbleFCDriver( configuration=configuration) - self.mock_client_service.service.login.return_value = ( - FAKE_POSITIVE_LOGIN_RESPONSE_1) + mock_login_response = mock_urllib2.post.return_value + mock_login_response = mock.MagicMock() + mock_login_response.status_code.return_value = 200 + mock_login_response.json.return_value = ( + FAKE_LOGIN_POST_RESPONSE) self.driver.do_setup(context.get_admin_context()) + self.driver.APIExecutor.login() func(self, *args, **kwargs) return inner_clent_mock return client_mock_wrapper @@ -248,22 +245,10 @@ class NimbleDriverLoginTestCase(NimbleDriverBaseTestCase): @mock.patch.object(obj_volume.VolumeList, 'get_all_by_host', mock.Mock(return_value=[])) @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( - 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) + "nimble", "nimble_pass", "10.18.108.55", 'default', '*')) def test_do_setup_positive(self): - expected_call_list = [ - mock.call.Client( - 'https://10.18.108.55/wsdl/NsGroupManagement.wsdl', - username='nimble', - password='nimble_pass')] - self.assertEqual(self.mock_client_class.method_calls, - expected_call_list) - expected_call_list = [mock.call.set_options( - location='https://10.18.108.55:5391/soap'), - mock.call.service.login( - req={'username': 'nimble', 'password': 'nimble_pass'})] - self.assertEqual( - self.mock_client_service.method_calls, - expected_call_list) + expected_call_list = [mock.call.login()] + self.mock_client_service.assert_has_calls(expected_call_list) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -272,26 +257,14 @@ class NimbleDriverLoginTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_expire_session_id(self): - self.mock_client_service.service.login.return_value = \ - FAKE_POSITIVE_LOGIN_RESPONSE_2 - self.mock_client_service.service.getNetConfig = mock.MagicMock( - side_effect=[ - FAKE_NEGATIVE_NETCONFIG_RESPONSE, - FAKE_POSITIVE_NETCONFIG_RESPONSE]) - self.driver.APIExecutor.get_netconfig("active") - expected_call_list = [mock.call.set_options( - location='https://10.18.108.55:5391/soap'), - mock.call.service.login( - req={ - 'username': 'nimble', 'password': 'nimble_pass'}), - mock.call.service.getNetConfig( - request={'name': 'active', - 'sid': 'a9b9aba7'}), - mock.call.service.login( - req={'username': 'nimble', - 'password': 'nimble_pass'}), - mock.call.service.getNetConfig( - request={'name': 'active', 'sid': 'a9f3eba7'})] + expected_call_list = [mock.call.login()] + self.mock_client_service.assert_has_calls(expected_call_list) + + self.driver.APIExecutor.get("groups") + expected_call_list = [mock.call.get_group_info(), + mock.call.login(), + mock.call.get("groups")] + self.assertEqual( self.mock_client_service.method_calls, expected_call_list) @@ -310,33 +283,33 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): 'nimble:perfpol-name': 'default', 'nimble:encryption': 'yes'})) @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( - 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) + NIMBLE_SAN_LOGIN, NIMBLE_SAN_PASS, NIMBLE_MANAGEMENT_IP, + 'default', '*')) def test_create_volume_positive(self): - self.mock_client_service.service.createVol.return_value = \ - FAKE_CREATE_VOLUME_POSITIVE_RESPONSE - self.mock_client_service.service.getVolInfo.return_value = \ - FAKE_GET_VOL_INFO_RESPONSE - self.mock_client_service.service.getNetConfig.return_value = \ - FAKE_POSITIVE_NETCONFIG_RESPONSE + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE) + self.mock_client_service.get_netconfig.return_value = ( + FAKE_POSITIVE_NETCONFIG_RESPONSE) + self.assertEqual({ - 'provider_location': '172.18.108.21:3260 iqn.test 0', + 'provider_location': '172.18.108.21:3260 iqn.test', 'provider_auth': None}, self.driver.create_volume({'name': 'testvolume', 'size': 1, 'volume_type_id': None, 'display_name': '', 'display_description': ''})) - self.mock_client_service.service.createVol.assert_called_once_with( - request={ - 'attr': {'snap-quota': sys.maxsize, - 'warn-level': 858993459, - 'name': 'testvolume', 'reserve': 0, - 'online': True, 'pool-name': 'default', - 'size': 1073741824, 'quota': 1073741824, - 'perfpol-name': 'default', 'description': '', - 'agent-type': 5, 'encryptionAttr': {'cipher': 3}, - 'multi-initiator': 'false'}, - 'sid': 'a9b9aba7'}) + + self.mock_client_service.create_vol.assert_called_once_with( + {'name': 'testvolume', + 'size': 1, + 'volume_type_id': None, + 'display_name': '', + 'display_description': ''}, + 'default', + False, + 'iSCSI', + False) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -350,36 +323,34 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_create_volume_encryption_positive(self): - self.mock_client_service.service.createVol.return_value = \ - FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_ENCRYPTION - self.mock_client_service.service.getVolInfo.return_value = \ - FAKE_GET_VOL_INFO_RESPONSE - self.mock_client_service.service.getNetConfig.return_value = \ - FAKE_POSITIVE_NETCONFIG_RESPONSE + self.mock_client_service._execute_create_vol.return_value = ( + FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_ENCRYPTION) + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE) + self.mock_client_service.get_netconfig.return_value = ( + FAKE_POSITIVE_NETCONFIG_RESPONSE) + volume = {'name': 'testvolume-encryption', + 'size': 1, + 'volume_type_id': FAKE_TYPE_ID, + 'display_name': '', + 'display_description': ''} self.assertEqual({ - 'provider_location': '172.18.108.21:3260 iqn.test 0', + 'provider_location': '172.18.108.21:3260 iqn.test', 'provider_auth': None}, - self.driver.create_volume({'name': 'testvolume-encryption', - 'size': 1, - 'volume_type_id': FAKE_TYPE_ID, - 'display_name': '', - 'display_description': ''})) + self.driver.create_volume(volume)) - mock_volume_type = volume_types.get_volume_type_extra_specs - mock_volume_type.assert_called_once_with(FAKE_TYPE_ID) - - self.mock_client_service.service.createVol.assert_called_once_with( - request={ - 'attr': {'snap-quota': sys.maxsize, - 'warn-level': 858993459, - 'name': 'testvolume-encryption', 'reserve': 0, - 'online': True, 'pool-name': 'default', - 'size': 1073741824, 'quota': 1073741824, - 'perfpol-name': 'default', 'description': '', - 'agent-type': 5, 'encryptionAttr': {'cipher': 2}, - 'multi-initiator': 'false'}, - 'sid': 'a9b9aba7'}) + self.mock_client_service.create_vol.assert_called_once_with( + {'name': 'testvolume-encryption', + 'size': 1, + 'volume_type_id': FAKE_TYPE_ID, + 'display_name': '', + 'display_description': '', + }, + 'default', + False, + 'iSCSI', + False) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -393,15 +364,15 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_create_volume_perfpolicy_positive(self): - self.mock_client_service.service.createVol.return_value = \ - FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_PERFPOLICY - self.mock_client_service.service.getVolInfo.return_value = \ - FAKE_GET_VOL_INFO_RESPONSE - self.mock_client_service.service.getNetConfig.return_value = \ - FAKE_POSITIVE_NETCONFIG_RESPONSE + self.mock_client_service._execute_create_vol.return_value = ( + FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_PERF_POLICY) + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE) + self.mock_client_service.get_netconfig.return_value = ( + FAKE_POSITIVE_NETCONFIG_RESPONSE) self.assertEqual( - {'provider_location': '172.18.108.21:3260 iqn.test 0', + {'provider_location': '172.18.108.21:3260 iqn.test', 'provider_auth': None}, self.driver.create_volume({'name': 'testvolume-perfpolicy', 'size': 1, @@ -409,20 +380,17 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): 'display_name': '', 'display_description': ''})) - mock_volume_type = volume_types.get_volume_type_extra_specs - mock_volume_type.assert_called_once_with(FAKE_TYPE_ID) - - self.mock_client_service.service.createVol.assert_called_once_with( - request={ - 'attr': {'snap-quota': sys.maxsize, - 'warn-level': 858993459, - 'name': 'testvolume-perfpolicy', 'reserve': 0, - 'online': True, 'pool-name': 'default', - 'size': 1073741824, 'quota': 1073741824, - 'perfpol-name': 'VMware ESX', 'description': '', - 'agent-type': 5, 'encryptionAttr': {'cipher': 3}, - 'multi-initiator': 'false'}, - 'sid': 'a9b9aba7'}) + self.mock_client_service.create_vol.assert_called_once_with( + {'name': 'testvolume-perfpolicy', + 'size': 1, + 'volume_type_id': FAKE_TYPE_ID, + 'display_name': '', + 'display_description': '', + }, + 'default', + False, + 'iSCSI', + False) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -436,36 +404,33 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_create_volume_multi_initiator_positive(self): - self.mock_client_service.service.createVol.return_value = \ - FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_PERFPOLICY - self.mock_client_service.service.getVolInfo.return_value = \ - FAKE_GET_VOL_INFO_RESPONSE - self.mock_client_service.service.getNetConfig.return_value = \ - FAKE_POSITIVE_NETCONFIG_RESPONSE + self.mock_client_service._execute_create_vol.return_value = ( + FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_MULTI_INITIATOR) + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE) + self.mock_client_service.get_netconfig.return_value = ( + FAKE_POSITIVE_NETCONFIG_RESPONSE) self.assertEqual( - {'provider_location': '172.18.108.21:3260 iqn.test 0', + {'provider_location': '172.18.108.21:3260 iqn.test', 'provider_auth': None}, - self.driver.create_volume({'name': 'testvolume-perfpolicy', + self.driver.create_volume({'name': 'testvolume-multi-initiator', 'size': 1, 'volume_type_id': FAKE_TYPE_ID, 'display_name': '', 'display_description': ''})) - mock_volume_type = volume_types.get_volume_type_extra_specs - mock_volume_type.assert_called_once_with(FAKE_TYPE_ID) - - self.mock_client_service.service.createVol.assert_called_once_with( - request={ - 'attr': {'snap-quota': sys.maxsize, - 'warn-level': 858993459, - 'name': 'testvolume-perfpolicy', 'reserve': 0, - 'online': True, 'pool-name': 'default', - 'size': 1073741824, 'quota': 1073741824, - 'perfpol-name': 'default', 'description': '', - 'agent-type': 5, 'encryptionAttr': {'cipher': 3}, - 'multi-initiator': 'true'}, - 'sid': 'a9b9aba7'}) + self.mock_client_service.create_vol.assert_called_once_with( + {'name': 'testvolume-multi-initiator', + 'size': 1, + 'volume_type_id': FAKE_TYPE_ID, + 'display_name': '', + 'display_description': '', + }, + 'default', + False, + 'iSCSI', + False) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -473,15 +438,21 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): mock.Mock(return_value=[])) @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) + @mock.patch.object(volume_types, 'get_volume_type_extra_specs', + mock.Mock(type_id=FAKE_TYPE_ID, return_value={ + 'nimble:perfpol-name': 'default', + 'nimble:encryption': 'no', + 'nimble:multi-initiator': 'true'})) def test_create_volume_negative(self): - self.mock_client_service.service.createVol.return_value = \ - FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE + self.mock_client_service.get_vol_info.side_effect = ( + FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE) + self.assertRaises( exception.VolumeBackendAPIException, self.driver.create_volume, {'name': 'testvolume', 'size': 1, - 'volume_type_id': None, + 'volume_type_id': FAKE_TYPE_ID, 'display_name': '', 'display_description': ''}) @@ -492,8 +463,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_create_volume_encryption_negative(self): - self.mock_client_service.service.createVol.return_value = \ - FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE_ENCRYPTION + self.mock_client_service.get_vol_info.side_effect = ( + FAKE_CREATE_VOLUME_NEGATIVE_ENCRYPTION) self.assertRaises( exception.VolumeBackendAPIException, self.driver.create_volume, @@ -510,8 +481,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_create_volume_perfpolicy_negative(self): - self.mock_client_service.service.createVol.return_value = \ - FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE_PERFPOLICY + self.mock_client_service.get_vol_info.side_effect = ( + FAKE_CREATE_VOLUME_NEGATIVE_PERFPOLICY) self.assertRaises( exception.VolumeBackendAPIException, self.driver.create_volume, @@ -530,20 +501,15 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @mock.patch(NIMBLE_ISCSI_DRIVER + ".is_volume_backup_clone", mock.Mock( return_value = ['', ''])) def test_delete_volume(self): - self.mock_client_service.service.onlineVol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.deleteVol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.dissocProtPol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE + self.mock_client_service.online_vol.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) + self.mock_client_service.delete_vol.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) self.driver.delete_volume({'name': 'testvolume'}) - expected_calls = [mock.call.service.onlineVol( - request={ - 'online': False, 'name': 'testvolume', 'sid': 'a9b9aba7'}), - mock.call.service.dissocProtPol( - request={'vol-name': 'testvolume', 'sid': 'a9b9aba7'}), - mock.call.service.deleteVol( - request={'name': 'testvolume', 'sid': 'a9b9aba7'})] + expected_calls = [mock.call.online_vol( + 'testvolume', False), + mock.call.delete_vol('testvolume')] + self.mock_client_service.assert_has_calls(expected_calls) @mock.patch(NIMBLE_URLLIB2) @@ -555,34 +521,24 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @mock.patch.object(obj_volume.VolumeList, 'get_all_by_host') def test_delete_volume_with_backup(self, mock_volume_list): mock_volume_list.return_value = [] - self.mock_client_service.service.onlineVol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.deleteVol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.dissocProtPol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.onlineSnap.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.deleteSnap.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE + self.mock_client_service.online_vol.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) + self.mock_client_service.delete_vol.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) + self.mock_client_service.online_snap.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) + self.mock_client_service.delete_snap.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) self.driver.delete_volume({'name': 'testvolume'}) - expected_calls = [mock.call.service.onlineVol( - request={ - 'online': False, 'name': 'testvolume', 'sid': 'a9b9aba7'}), - mock.call.service.dissocProtPol( - request={'vol-name': 'testvolume', 'sid': 'a9b9aba7'}), - mock.call.service.deleteVol( - request={'name': 'testvolume', 'sid': 'a9b9aba7'}), - mock.call.service.onlineSnap( - request={'vol': 'volume-' + fake.VOLUME_ID, - 'name': 'test-backup-snap', - 'online': False, - 'sid': 'a9b9aba7'}), - mock.call.service.deleteSnap( - request={'vol': 'volume-' + fake.VOLUME_ID, - 'name': 'test-backup-snap', - 'sid': 'a9b9aba7'})] + expected_calls = [mock.call.online_vol( + 'testvolume', False), + mock.call.delete_vol('testvolume'), + mock.call.online_snap('volume-' + fake.VOLUME_ID, + False, + 'test-backup-snap'), + mock.call.delete_snap('volume-' + fake.VOLUME_ID, + 'test-backup-snap')] self.mock_client_service.assert_has_calls(expected_calls) @@ -593,18 +549,12 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_extend_volume(self): - self.mock_client_service.service.editVol.return_value = \ - FAKE_CREATE_VOLUME_POSITIVE_RESPONSE + self.mock_client_service.edit_vol.return_value = ( + FAKE_CREATE_VOLUME_POSITIVE_RESPONSE) self.driver.extend_volume({'name': 'testvolume'}, 5) - self.mock_client_service.service.editVol.assert_called_once_with( - request={'attr': {'size': 5368709120, - 'snap-quota': sys.maxsize, - 'warn-level': 4294967296, - 'reserve': 0, - 'quota': 5368709120}, - 'mask': 884, - 'name': 'testvolume', - 'sid': 'a9b9aba7'}) + + self.mock_client_service.edit_vol.assert_called_once_with( + 'testvolume', FAKE_EXTEND_VOLUME_PARAMS) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -621,14 +571,14 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): def test_create_cloned_volume(self, mock_random, mock_volume_list): mock_random.sample.return_value = fake.VOLUME_ID mock_volume_list.return_value = [] - self.mock_client_service.service.snapVol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.cloneVol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.getVolInfo.return_value = \ - FAKE_GET_VOL_INFO_RESPONSE - self.mock_client_service.service.getNetConfig.return_value = \ - FAKE_POSITIVE_NETCONFIG_RESPONSE + self.mock_client_service.snap_vol.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) + self.mock_client_service.clone_vol.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE) + self.mock_client_service.get_netconfig.return_value = ( + FAKE_POSITIVE_NETCONFIG_RESPONSE) volume = obj_volume.Volume(context.get_admin_context(), id=fake.VOLUME_ID, @@ -642,33 +592,27 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): _name_id=None, size=5.0) self.assertEqual({ - 'provider_location': '172.18.108.21:3260 iqn.test 0', + 'provider_location': '172.18.108.21:3260 iqn.test', 'provider_auth': None}, self.driver.create_cloned_volume(volume, src_volume)) - expected_calls = [mock.call.service.snapVol( - request={ - 'vol': "volume-" + fake.VOLUME2_ID, - 'snapAttr': {'name': 'openstack-clone-volume-' + - fake.VOLUME_ID + - "-" + fake.VOLUME_ID, - 'description': ''}, - 'sid': 'a9b9aba7'}), - mock.call.service.cloneVol( - request={ - 'snap-name': 'openstack-clone-volume-' + fake.VOLUME_ID + - "-" + fake.VOLUME_ID, - 'attr': {'snap-quota': sys.maxsize, - 'name': 'volume-' + fake.VOLUME_ID, - 'quota': 5368709120, - 'reserve': 5368709120, - 'online': True, - 'warn-level': 4294967296, - 'encryptionAttr': {'cipher': 2}, - 'multi-initiator': 'false', - 'perfpol-name': 'default', - 'agent-type': 5}, - 'name': 'volume-' + fake.VOLUME2_ID, - 'sid': 'a9b9aba7'})] + + expected_calls = [mock.call.snap_vol( + {'volume_name': "volume-" + fake.VOLUME2_ID, + 'name': 'openstack-clone-volume-' + fake.VOLUME_ID + "-" + + fake.VOLUME_ID, + 'volume_size': src_volume['size'], + 'display_name': volume['display_name'], + 'display_description': ''}), + mock.call.clone_vol(volume, + {'volume_name': "volume-" + fake.VOLUME2_ID, + 'name': 'openstack-clone-volume-' + + fake.VOLUME_ID + "-" + + fake.VOLUME_ID, + 'volume_size': src_volume['size'], + 'display_name': volume['display_name'], + 'display_description': ''}, + True, False, 'iSCSI')] + self.mock_client_service.assert_has_calls(expected_calls) @mock.patch(NIMBLE_URLLIB2) @@ -678,33 +622,25 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_manage_volume_positive(self): - self.mock_client_service.service.getNetConfig.return_value = ( + self.mock_client_service.get_netconfig.return_value = ( FAKE_POSITIVE_NETCONFIG_RESPONSE) - self.mock_client_service.service.getVolInfo.return_value = ( - FAKE_GET_VOL_INFO_RESPONSE) - self.mock_client_service.service.onlineVol.return_value = ( + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE_MANAGE) + self.mock_client_service.online_vol.return_value = ( FAKE_GENERIC_POSITIVE_RESPONSE) - self.mock_client_service.service.editVol.return_value = ( + self.mock_client_service.edit_vol.return_value = ( FAKE_CREATE_VOLUME_POSITIVE_RESPONSE) self.assertEqual({ - 'provider_location': '172.18.108.21:3260 iqn.test 0', + 'provider_location': '172.18.108.21:3260 iqn.test', 'provider_auth': None}, - self.driver.manage_existing({'name': 'volume-abcdef'}, + self.driver.manage_existing({'name': 'volume-abcdef', + 'id': fake.VOLUME_ID, + 'agent_type': None}, {'source-name': 'test-vol'})) - expected_calls = [ - mock.call.service.editVol( - request={ - 'attr': { - 'name': 'volume-abcdef', 'agent-type': 5}, - 'mask': 262145, - 'name': 'test-vol', - 'sid': 'a9b9aba7'}), - mock.call.service.onlineVol( - request={'online': True, - 'name': 'volume-abcdef', - 'sid': 'a9b9aba7'} - ) - ] + expected_calls = [mock.call.edit_vol( + 'test-vol', {'data': {'agent_type': 'openstack', + 'name': 'volume-abcdef'}}), + mock.call.online_vol('volume-abcdef', True)] self.mock_client_service.assert_has_calls(expected_calls) @mock.patch(NIMBLE_URLLIB2) @@ -714,9 +650,9 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_manage_volume_which_is_online(self): - self.mock_client_service.service.getNetConfig.return_value = ( + self.mock_client_service.get_netconfig.return_value = ( FAKE_POSITIVE_NETCONFIG_RESPONSE) - self.mock_client_service.service.getVolInfo.return_value = ( + self.mock_client_service.get_vol_info.return_value = ( FAKE_GET_VOL_INFO_ONLINE) self.assertRaises( exception.InvalidVolume, @@ -731,9 +667,9 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_manage_volume_get_size(self): - self.mock_client_service.service.getNetConfig.return_value = ( + self.mock_client_service.get_netconfig.return_value = ( FAKE_POSITIVE_NETCONFIG_RESPONSE) - self.mock_client_service.service.getVolInfo.return_value = ( + self.mock_client_service.get_vol_info.return_value = ( FAKE_GET_VOL_INFO_ONLINE) size = self.driver.manage_existing_get_size( {'name': 'volume-abcdef'}, {'source-name': 'test-vol'}) @@ -759,8 +695,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_manage_volume_with_nonexistant_volume(self): - self.mock_client_service.service.getVolInfo.return_value = ( - FAKE_GET_VOL_INFO_ERROR) + self.mock_client_service.get_vol_info.side_effect = ( + FAKE_VOLUME_INFO_NEGATIVE_RESPONSE) self.assertRaises( exception.VolumeBackendAPIException, self.driver.manage_existing, @@ -774,8 +710,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_manage_volume_with_wrong_agent_type(self): - self.mock_client_service.service.getVolInfo.return_value = ( - FAKE_GET_VOL_INFO_RESPONSE_WITH_SET_AGENT_TYPE) + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE) self.assertRaises( exception.ManageExistingAlreadyManaged, self.driver.manage_existing, @@ -789,23 +725,18 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_unmanage_volume_positive(self): - self.mock_client_service.service.getVolInfo.return_value = ( - FAKE_GET_VOL_INFO_RESPONSE_WITH_SET_AGENT_TYPE) - self.mock_client_service.service.editVol.return_value = ( + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE) + self.mock_client_service.edit_vol.return_value = ( FAKE_CREATE_VOLUME_POSITIVE_RESPONSE) self.driver.unmanage({'name': 'volume-abcdef'}) expected_calls = [ - mock.call.service.editVol( - request={'attr': {'agent-type': 1}, - 'mask': 262144, - 'name': 'volume-abcdef', - 'sid': 'a9b9aba7'}), - mock.call.service.onlineVol( - request={'online': False, - 'name': 'volume-abcdef', - 'sid': 'a9b9aba7'} - ) - ] + mock.call.edit_vol( + 'volume-abcdef', + {'data': {'agent_type': 'none'}}), + + mock.call.online_vol('volume-abcdef', False)] + self.mock_client_service.assert_has_calls(expected_calls) @mock.patch(NIMBLE_URLLIB2) @@ -815,8 +746,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_unmanage_with_invalid_volume(self): - self.mock_client_service.service.getVolInfo.return_value = ( - FAKE_GET_VOL_INFO_ERROR) + self.mock_client_service.get_vol_info.side_effect = ( + FAKE_VOLUME_INFO_NEGATIVE_RESPONSE) self.assertRaises( exception.VolumeBackendAPIException, self.driver.unmanage, @@ -830,8 +761,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_unmanage_with_invalid_agent_type(self): - self.mock_client_service.service.getVolInfo.return_value = ( - FAKE_GET_VOL_INFO_RESPONSE) + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_ONLINE) self.assertRaises( exception.InvalidVolume, self.driver.unmanage, @@ -845,8 +776,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_get_volume_stats(self): - self.mock_client_service.service.getGroupConfig.return_value = \ - FAKE_POSITIVE_GROUP_CONFIG_RESPONSE + self.mock_client_service.get_group_info.return_value = ( + FAKE_POSITIVE_GROUP_INFO_RESPONSE) expected_res = {'driver_version': DRIVER_VERSION, 'vendor_name': 'Nimble', 'volume_backend_name': 'NIMBLE', @@ -867,23 +798,24 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_is_volume_backup_clone(self): - self.mock_client_service.service.getVolInfo.return_value = \ - FAKE_GET_VOL_INFO_BACKUP_RESPONSE - self.mock_client_service.service.getSnapInfo.return_value = \ - FAKE_GET_SNAP_INFO_BACKUP_RESPONSE + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_BACKUP_RESPONSE) + self.mock_client_service.get_snap_info_by_id.return_value = ( + FAKE_GET_SNAP_INFO_BACKUP_RESPONSE) + self.mock_client_service.get_snap_info_detail.return_value = ( + FAKE_GET_SNAP_INFO_BACKUP_RESPONSE) + self.mock_client_service.get_volume_name.return_value = ( + 'volume-' + fake.VOLUME2_ID) + volume = obj_volume.Volume(context.get_admin_context(), id=fake.VOLUME_ID, _name_id=None) - self.assertEqual(("test-backup-snap", "volume-" + fake.VOLUME_ID), + self.assertEqual(("test-backup-snap", "volume-" + fake.VOLUME2_ID), self.driver.is_volume_backup_clone(volume)) expected_calls = [ - mock.call.service.getVolInfo( - request={'name': 'volume-' + fake.VOLUME_ID, - 'sid': 'a9b9aba7'}), - mock.call.service.getSnapInfo( - request={'sid': 'a9b9aba7', - 'vol': 'volume-' + fake.VOLUME2_ID, - 'name': 'test-backup-snap'}) + mock.call.get_vol_info('volume-' + fake.VOLUME_ID), + mock.call.get_snap_info_by_id('test-backup-snap', + 'volume-' + fake.VOLUME2_ID) ] self.mock_client_service.assert_has_calls(expected_calls) @@ -899,20 +831,16 @@ class NimbleDriverSnapshotTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_create_snapshot(self): - self.mock_client_service.service.snapVol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE + self.mock_client_service.snap_vol.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) self.driver.create_snapshot( {'volume_name': 'testvolume', 'name': 'testvolume-snap1', - 'display_name': '', - 'display_description': ''}) - self.mock_client_service.service.snapVol.assert_called_once_with( - request={'vol': 'testvolume', - 'snapAttr': {'name': 'testvolume-snap1', - 'description': - '' - }, - 'sid': 'a9b9aba7'}) + 'display_name': ''}) + self.mock_client_service.snap_vol.assert_called_once_with( + {'volume_name': 'testvolume', + 'name': 'testvolume-snap1', + 'display_name': ''}) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -921,22 +849,17 @@ class NimbleDriverSnapshotTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_delete_snapshot(self): - self.mock_client_service.service.onlineSnap.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.deleteSnap.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE + self.mock_client_service.online_snap.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) + self.mock_client_service.delete_snap.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) self.driver.delete_snapshot( {'volume_name': 'testvolume', 'name': 'testvolume-snap1'}) - expected_calls = [mock.call.service.onlineSnap( - request={ - 'vol': 'testvolume', - 'online': False, - 'name': 'testvolume-snap1', - 'sid': 'a9b9aba7'}), - mock.call.service.deleteSnap(request={'vol': 'testvolume', - 'name': 'testvolume-snap1', - 'sid': 'a9b9aba7'})] + expected_calls = [mock.call.online_snap( + 'testvolume', False, 'testvolume-snap1'), + mock.call.delete_snap('testvolume', + 'testvolume-snap1')] self.mock_client_service.assert_has_calls(expected_calls) @mock.patch(NIMBLE_URLLIB2) @@ -951,14 +874,14 @@ class NimbleDriverSnapshotTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_create_volume_from_snapshot(self): - self.mock_client_service.service.cloneVol.return_value = \ - FAKE_GENERIC_POSITIVE_RESPONSE - self.mock_client_service.service.getVolInfo.return_value = \ - FAKE_GET_VOL_INFO_RESPONSE - self.mock_client_service.service.getNetConfig.return_value = \ - FAKE_POSITIVE_NETCONFIG_RESPONSE + self.mock_client_service.clone_vol.return_value = ( + FAKE_GENERIC_POSITIVE_RESPONSE) + self.mock_client_service.get_vol_info.return_value = ( + FAKE_GET_VOL_INFO_RESPONSE) + self.mock_client_service.get_netconfig.return_value = ( + FAKE_POSITIVE_NETCONFIG_RESPONSE) self.assertEqual({ - 'provider_location': '172.18.108.21:3260 iqn.test 0', + 'provider_location': '172.18.108.21:3260 iqn.test', 'provider_auth': None}, self.driver.create_volume_from_snapshot( {'name': 'clone-testvolume', @@ -968,29 +891,22 @@ class NimbleDriverSnapshotTestCase(NimbleDriverBaseTestCase): 'name': 'testvolume-snap1', 'volume_size': 1})) expected_calls = [ - mock.call.service.cloneVol( - request={'snap-name': 'testvolume-snap1', - 'attr': {'snap-quota': sys.maxsize, - 'name': 'clone-testvolume', - 'quota': 1073741824, - 'online': True, - 'reserve': 0, - 'warn-level': 858993459, - 'perfpol-name': 'default', - 'encryptionAttr': {'cipher': 2}, - 'multi-initiator': 'false', - 'agent-type': 5}, - 'name': 'testvolume', - 'sid': 'a9b9aba7'}), - mock.call.service.editVol( - request={'attr': {'size': 2147483648, - 'snap-quota': sys.maxsize, - 'warn-level': 1717986918, - 'reserve': 0, - 'quota': 2147483648}, - 'mask': 884, - 'name': 'clone-testvolume', - 'sid': 'a9b9aba7'})] + mock.call.clone_vol( + {'name': 'clone-testvolume', + 'volume_type_id': FAKE_TYPE_ID, + 'size': 2}, + {'volume_name': 'testvolume', + 'name': 'testvolume-snap1', + 'volume_size': 1}, + False, + False, + 'iSCSI'), + mock.call.edit_vol('clone-testvolume', + {'data': {'size': 2048, + 'snap_limit': sys.maxsize, + 'warn_level': 80, + 'reserve': 0, + 'limit': 100}})] self.mock_client_service.assert_has_calls(expected_calls) @@ -1005,43 +921,26 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_initialize_connection_igroup_exist(self): - self.mock_client_service.service.getInitiatorGrpList.return_value = \ - FAKE_IGROUP_LIST_RESPONSE + self.mock_client_service.get_initiator_grp_list.return_value = ( + FAKE_IGROUP_LIST_RESPONSE) expected_res = { 'driver_volume_type': 'iscsi', 'data': { - 'target_lun': 14, 'volume_id': 12, 'target_iqn': '13', - 'target_discovered': False, + 'target_lun': 0, 'target_portal': '12'}} self.assertEqual( expected_res, self.driver.initialize_connection( {'name': 'test-volume', - 'provider_location': '12 13 14', + 'provider_location': '12 13', 'id': 12}, {'initiator': 'test-initiator1'})) - expected_call_list = [mock.call.set_options( - location='https://10.18.108.55:5391/soap'), - mock.call.service.login( - req={ - 'username': 'nimble', 'password': 'nimble_pass'}), - mock.call.service.getInitiatorGrpList( - request={'sid': 'a9b9aba7'}), - mock.call.service.addVolAcl( - request={'volname': 'test-volume', - 'apply-to': 3, - 'chapuser': '*', - 'initiatorgrp': 'test-igrp2', - 'sid': 'a9b9aba7'})] - self.assertEqual( - self.mock_client_service.method_calls, - expected_call_list) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) - @mock.patch.object(obj_volume.VolumeList, 'get_all', + @mock.patch.object(obj_volume.VolumeList, 'get_all_by_host', mock.Mock(return_value=[])) @NimbleDriverBaseTestCase.client_mock_decorator_fc(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) @@ -1051,7 +950,7 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): mock_lun_number): mock_lun_number.return_value = 13 mock_wwpns.return_value = ["1111111111111101"] - self.mock_client_service.service.getInitiatorGrpList.return_value = ( + self.mock_client_service.get_initiator_grp_list.return_value = ( FAKE_IGROUP_LIST_RESPONSE_FC) expected_res = { 'driver_volume_type': 'fibre_channel', @@ -1069,22 +968,6 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): 'id': 12}, {'initiator': 'test-initiator1', 'wwpns': ['1000000000000000']})) - expected_call_list = [mock.call.set_options( - location='https://10.18.108.55:5391/soap'), - mock.call.service.login( - req={ - 'username': 'nimble', 'password': 'nimble_pass'}), - mock.call.service.getInitiatorGrpList( - request={'sid': 'a9b9aba7'}), - mock.call.service.addVolAcl( - request={'volname': 'test-volume', - 'apply-to': 3, - 'chapuser': '*', - 'initiatorgrp': 'test-igrp2', - 'sid': 'a9b9aba7'})] - self.assertEqual( - self.mock_client_service.method_calls, - expected_call_list) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -1095,44 +978,26 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): @mock.patch(NIMBLE_RANDOM) def test_initialize_connection_igroup_not_exist(self, mock_random): mock_random.sample.return_value = 'abcdefghijkl' - self.mock_client_service.service.getInitiatorGrpList.return_value = \ - FAKE_IGROUP_LIST_RESPONSE + self.mock_client_service.get_initiator_grp_list.return_value = ( + FAKE_IGROUP_LIST_RESPONSE) expected_res = { 'driver_volume_type': 'iscsi', 'data': { - 'target_lun': 14, + 'target_lun': 0, 'volume_id': 12, 'target_iqn': '13', - 'target_discovered': False, 'target_portal': '12'}} self.assertEqual( expected_res, self.driver.initialize_connection( {'name': 'test-volume', - 'provider_location': '12 13 14', + 'provider_location': '12 13', 'id': 12}, {'initiator': 'test-initiator3'})) - expected_calls = [ - mock.call.service.getInitiatorGrpList( - request={'sid': 'a9b9aba7'}), - mock.call.service.createInitiatorGrp( - request={ - 'attr': {'initiator-list': [{'name': 'test-initiator3', - 'label': 'test-initiator3'}], - 'name': 'openstack-abcdefghijkl'}, - 'sid': 'a9b9aba7'}), - mock.call.service.addVolAcl( - request={'volname': 'test-volume', 'apply-to': 3, - 'chapuser': '*', - 'initiatorgrp': 'openstack-abcdefghijkl', - 'sid': 'a9b9aba7'})] - self.mock_client_service.assert_has_calls( - self.mock_client_service.method_calls, - expected_calls) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) - @mock.patch.object(obj_volume.VolumeList, 'get_all', + @mock.patch.object(obj_volume.VolumeList, 'get_all_by_host', mock.Mock(return_value=[])) @NimbleDriverBaseTestCase.client_mock_decorator_fc(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) @@ -1145,7 +1010,7 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): mock_random.sample.return_value = 'abcdefghijkl' mock_lun_number.return_value = 13 mock_wwpns.return_value = ["1111111111111101"] - self.mock_client_service.service.getInitiatorGrpList.return_value = ( + self.mock_client_service.get_initiator_grp_list.return_value = ( FAKE_IGROUP_LIST_RESPONSE_FC) expected_res = { 'driver_volume_type': 'fibre_channel', @@ -1164,22 +1029,6 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): 'id': 12}, {'initiator': 'test-initiator3', 'wwpns': ['1000000000000000']})) - expected_calls = [ - mock.call.service.getInitiatorGrpList( - request={'sid': 'a9b9aba7'}), - mock.call.service.createInitiatorGrp( - request={ - 'attr': {'initiator-list': [{'wwn': '1000000000000000'}], - 'name': 'openstack-abcdefghijkl'}, - 'sid': 'a9b9aba7'}), - mock.call.service.addVolAcl( - request={'volname': 'test-volume', 'apply-to': 3, - 'chapuser': '*', - 'initiatorgrp': 'openstack-abcdefghijkl', - 'sid': 'a9b9aba7'})] - self.mock_client_service.assert_has_calls( - self.mock_client_service.method_calls, - expected_calls) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) @@ -1188,36 +1037,31 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_terminate_connection_positive(self): - self.mock_client_service.service.getInitiatorGrpList.return_value = \ - FAKE_IGROUP_LIST_RESPONSE + self.mock_client_service.get_initiator_grp_list.return_value = ( + FAKE_IGROUP_LIST_RESPONSE) self.driver.terminate_connection( {'name': 'test-volume', - 'provider_location': '12 13 14', + 'provider_location': '12 13', 'id': 12}, {'initiator': 'test-initiator1'}) - expected_calls = [mock.call.service.getInitiatorGrpList( - request={'sid': 'a9b9aba7'}), - mock.call.service.removeVolAcl( - request={'volname': 'test-volume', - 'apply-to': 3, - 'chapuser': '*', - 'initiatorgrp': {'initiator-list': - [{'name': 'test-initiator1'}]}, - 'sid': 'a9b9aba7'})] + expected_calls = [mock.call._get_igroupname_for_initiator( + 'test-initiator1'), + mock.call.remove_acl({'name': 'test-volume'}, + 'test-igrp1')] self.mock_client_service.assert_has_calls( self.mock_client_service.method_calls, expected_calls) @mock.patch(NIMBLE_URLLIB2) @mock.patch(NIMBLE_CLIENT) - @mock.patch.object(obj_volume.VolumeList, 'get_all', + @mock.patch.object(obj_volume.VolumeList, 'get_all_by_host', mock.Mock(return_value=[])) @NimbleDriverBaseTestCase.client_mock_decorator_fc(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) @mock.patch(NIMBLE_FC_DRIVER + ".get_wwpns_from_array") def test_terminate_connection_positive_fc(self, mock_wwpns): mock_wwpns.return_value = ["1111111111111101"] - self.mock_client_service.service.getInitiatorGrpList.return_value = ( + self.mock_client_service.get_initiator_grp_list.return_value = ( FAKE_IGROUP_LIST_RESPONSE_FC) self.driver.terminate_connection( {'name': 'test-volume', @@ -1225,15 +1069,11 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): 'id': 12}, {'initiator': 'test-initiator1', 'wwpns': ['1000000000000000']}) - expected_calls = [mock.call.service.getInitiatorGrpList( - request={'sid': 'a9b9aba7'}), - mock.call.service.removeVolAcl( - request={'volname': 'test-volume', - 'apply-to': 3, - 'chapuser': '*', - 'initiatorgrp': {'initiator-list': - [{'wwn': '1000000000000000'}]}, - 'sid': 'a9b9aba7'})] + expected_calls = [ + mock.call.get_igroupname_for_initiator_fc( + "10:00:00:00:00:00:00:00"), + mock.call.remove_acl({'name': 'test-volume'}, + 'test-igrp1')] self.mock_client_service.assert_has_calls( self.mock_client_service.method_calls, expected_calls) @@ -1245,11 +1085,11 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase): @NimbleDriverBaseTestCase.client_mock_decorator(create_configuration( 'nimble', 'nimble_pass', '10.18.108.55', 'default', '*')) def test_terminate_connection_negative(self): - self.mock_client_service.service.getInitiatorGrpList.return_value = \ - FAKE_IGROUP_LIST_RESPONSE + self.mock_client_service.get_initiator_grp_list.return_value = ( + FAKE_IGROUP_LIST_RESPONSE) self.assertRaises( exception.VolumeDriverException, - self.driver.terminate_connection, { - 'name': 'test-volume', - 'provider_location': '12 13 14', 'id': 12}, + self.driver.terminate_connection, + {'name': 'test-volume', + 'provider_location': '12 13', 'id': 12}, {'initiator': 'test-initiator3'}) diff --git a/cinder/volume/drivers/nimble.py b/cinder/volume/drivers/nimble.py index 9e7f4f3de12..cd36c418121 100644 --- a/cinder/volume/drivers/nimble.py +++ b/cinder/volume/drivers/nimble.py @@ -15,26 +15,26 @@ """ Volume driver for Nimble Storage. -This driver supports Nimble Storage controller CS-Series. +This driver supports Nimble Storage controller CS-Series and Nimble AF Arrays. """ import abc +import eventlet import functools -import math +import json import random import re +import requests import six -import ssl import string import sys from oslo_config import cfg from oslo_log import log as logging from oslo_utils import units -from six.moves import urllib -from suds import client from cinder import exception +from cinder import utils from cinder.i18n import _, _LE, _LI, _LW from cinder import interface from cinder.objects import volume @@ -43,10 +43,9 @@ from cinder.volume.drivers.san import san from cinder.volume import volume_types from cinder.zonemanager import utils as fczm_utils - -DRIVER_VERSION = '3.1.0' -AES_256_XTS_CIPHER = 2 -DEFAULT_CIPHER = 3 +DRIVER_VERSION = "4.0.0" +AES_256_XTS_CIPHER = 'aes_256_xts' +DEFAULT_CIPHER = 'none' EXTRA_SPEC_ENCRYPTION = 'nimble:encryption' EXTRA_SPEC_PERF_POLICY = 'nimble:perfpol-name' EXTRA_SPEC_MULTI_INITIATOR = 'nimble:multi-initiator' @@ -54,26 +53,18 @@ DEFAULT_PERF_POLICY_SETTING = 'default' DEFAULT_ENCRYPTION_SETTING = 'no' DEFAULT_MULTI_INITIATOR_SETTING = 'false' DEFAULT_SNAP_QUOTA = sys.maxsize -VOL_EDIT_MASK = 4 + 16 + 32 + 64 + 256 + 512 -MANAGE_EDIT_MASK = 1 + 262144 -UNMANAGE_EDIT_MASK = 262144 -AGENT_TYPE_OPENSTACK = 5 -AGENT_TYPE_NONE = 1 -SOAP_PORT = 5391 -SM_ACL_APPLY_TO_BOTH = 3 -SM_ACL_CHAP_USER_ANY = '*' -SM_SUBNET_DATA = 3 -SM_SUBNET_MGMT_PLUS_DATA = 4 +BACKUP_VOL_PREFIX = 'backup-vol-' +AGENT_TYPE_OPENSTACK = 'openstack' +AGENT_TYPE_OPENSTACK_GST = 'openstackv2' +AGENT_TYPE_NONE = 'none' +SM_SUBNET_DATA = 'data' +SM_SUBNET_MGMT_PLUS_DATA = 'mgmt-data' +SM_STATE_MSG = "is already in requested state" LUN_ID = '0' -WARN_LEVEL = 0.8 -ISCSI_TARGET_PORT = ':3260' -FIBRE_CHANNEL_PROTOCOL = 2 +WARN_LEVEL = 80 +DEFAULT_SLEEP = 5 +NimbleDefaultVersion = 1 -# Work around for ubuntu_openssl_bug_965371. Python soap client suds -# throws the error ssl-certificate-verify-failed-error, workaround to disable -# ssl check for now -if hasattr(ssl, '_create_unverified_context'): - ssl._create_default_https_context = ssl._create_unverified_context LOG = logging.getLogger(__name__) @@ -83,8 +74,12 @@ nimble_opts = [ help='Nimble Controller pool name'), cfg.StrOpt('nimble_subnet_label', default='*', - help='Nimble Subnet Label'), ] - + help='Nimble Subnet Label'), + cfg.StrOpt('nimble_verify_certificate', + default=False, + help='Whether to verify Nimble SSL Certificate'), + cfg.StrOpt('nimble_verify_cert_path', + help='Path to Nimble Array SSL certificate'), ] CONF = cfg.CONF CONF.register_opts(nimble_opts) @@ -105,6 +100,7 @@ class NimbleBaseVolumeDriver(san.SanDriver): .. code-block:: none + 1.0 - Initial driver 1.1.1 - Updated VERSION to Nimble driver version 1.1.2 - Update snap-quota to unlimited @@ -115,8 +111,9 @@ class NimbleBaseVolumeDriver(san.SanDriver): 2.0.2 - Fixed supporting extra specs while cloning vols 3.0.0 - Newton Support for Force Backup 3.1.0 - Fibre Channel Support + 4.0.0 - Migrate from SOAP to REST API + Add support for Group Scoped Target """ - VERSION = DRIVER_VERSION # ThirdPartySystems wiki page @@ -126,8 +123,13 @@ class NimbleBaseVolumeDriver(san.SanDriver): super(NimbleBaseVolumeDriver, self).__init__(*args, **kwargs) self.APIExecutor = None self.group_stats = {} + self.api_protocol = None self._storage_protocol = None + self._group_target_enabled = False self.configuration.append_config_values(nimble_opts) + self.verify = False + if self.configuration.nimble_verify_certificate is True: + self.verify = self.configuration.nimble_verify_cert_path or True def _check_config(self): """Ensure that the flags we care about are set.""" @@ -137,89 +139,66 @@ class NimbleBaseVolumeDriver(san.SanDriver): raise exception.InvalidInput(reason=_('%s is not set.') % attr) - def _update_existing_vols_agent_type(self, context): - LOG.debug("Updating existing volumes to have " - "agent_type = 'OPENSTACK'") - all_vols = volume.VolumeList.get_all_by_host( - context, self.host, {'status': 'available'}) - for vol in all_vols: - try: - self.APIExecutor.edit_vol( - vol.name, - UNMANAGE_EDIT_MASK, - {'agent-type': AGENT_TYPE_OPENSTACK}) - except NimbleAPIException: - LOG.warning(_LW('Error updating agent-type for ' - 'volume %s.'), vol.name) - - def do_setup(self, context): - """Setup the Nimble Cinder volume driver.""" - self._check_config() - # Setup API Executor - try: - self.APIExecutor = NimbleAPIExecutor( - username=self.configuration.san_login, - password=self.configuration.san_password, - ip=self.configuration.san_ip) - except Exception: - LOG.error(_LE('Failed to create SOAP client.' - 'Check san_ip, username, password' - ' and make sure the array version is compatible')) - raise - self._update_existing_vols_agent_type(context) - - def _get_model_info(self, volume_name): - """Get model info for the volume.""" - return ( - {'provider_location': self._get_provider_location(volume_name), - 'provider_auth': None}) - - @abc.abstractmethod - def _get_provider_location(self, volume_name): - """Volume info for iSCSI and FC""" - - pass - def create_volume(self, volume): """Create a new volume.""" reserve = not self.configuration.san_thin_provision self.APIExecutor.create_vol( volume, - self.configuration.nimble_pool_name, reserve) + self.configuration.nimble_pool_name, reserve, + self._storage_protocol, + self._group_target_enabled) return self._get_model_info(volume['name']) def is_volume_backup_clone(self, volume): - """Check if the volume is created through cinder-backup workflow. + """check if the volume is created through cinder-backup workflow. - :param volume: reference to volume from delete_volume() + :param volume """ - vol_info = self.APIExecutor.get_vol_info(volume.name) - if vol_info['clone'] and vol_info['base-snap'] and vol_info[ - 'parent-vol']: - LOG.debug("Nimble base-snap exists for volume :%s", volume['name']) - volume_name_prefix = volume.name.replace(volume.id, "") - LOG.debug("volume_name_prefix : %s", volume_name_prefix) - snap_info = self.APIExecutor.get_snap_info(vol_info['base-snap'], - vol_info['parent-vol']) - if snap_info['description'] and "backup-vol-" in snap_info[ - 'description']: - parent_vol_id = vol_info['parent-vol' - ].replace(volume_name_prefix, "") - if "backup-vol-" + parent_vol_id in snap_info['description']: - LOG.info(_LI("nimble backup-snapshot exists name: %s"), - snap_info['name']) - return snap_info['name'], snap_info['vol'] + vol_info = self.APIExecutor.get_vol_info(volume['name']) + LOG.debug("is_clone: %(is_clone)s base_snap_id: %(snap)s, " + "parent_vol_id: %(vol)s", + {'is_clone': vol_info['clone'], + 'snap': vol_info['base_snap_id'], + 'vol': vol_info['parent_vol_id']}) + + if vol_info['base_snap_id'] and ( + vol_info['parent_vol_id'] is not None): + LOG.debug("Nimble base-snap exists for volume %(vol)s", + {'vol': volume['name']}) + volume_name_prefix = volume['name'].replace(volume['id'], "") + LOG.debug("volume_name_prefix : %(prefix)s", + {'prefix': volume_name_prefix}) + snap_id = self.APIExecutor.get_snap_info_by_id( + vol_info['base_snap_id'], + vol_info['parent_vol_id']) + snap_info = self.APIExecutor.get_snap_info_detail(snap_id['id']) + LOG.debug("snap_info description %(snap_info)s", + {'snap_info': snap_info['description']}) + if snap_info['description'] and BACKUP_VOL_PREFIX in ( + snap_info['description']): + # TODO(rkumar): get parent vol id from parent volume name + parent_vol_name = self.APIExecutor.get_volume_name( + vol_info['parent_vol_id']) + parent_vol_id = parent_vol_name. replace( + volume_name_prefix, "") + if BACKUP_VOL_PREFIX + parent_vol_id in snap_info[ + 'description']: + LOG.info(_LI('Nimble backup-snapshot exists name=%(' + 'name)s'), {'name': snap_info['name']}) + snap_vol_name = self.APIExecutor.get_volume_name( + snap_info['vol_id']) + LOG.debug("snap_vol_name %(snap)s", + {'snap': snap_vol_name}) + return snap_info['name'], snap_vol_name return "", "" def delete_volume(self, volume): """Delete the specified volume.""" - snap_name, vol_name = self.is_volume_backup_clone(volume) - self.APIExecutor.online_vol(volume['name'], False, - ignore_list=['SM-enoent']) - self.APIExecutor.dissociate_volcoll(volume['name'], - ignore_list=['SM-enoent']) - self.APIExecutor.delete_vol(volume['name'], ignore_list=['SM-enoent']) - + backup_snap_name, backup_vol_name = self.is_volume_backup_clone(volume) + eventlet.sleep(DEFAULT_SLEEP) + self.APIExecutor.online_vol(volume['name'], False) + LOG.debug("Deleting volume %(vol)s", {'vol': volume['name']}) + self.APIExecutor.delete_vol(volume['name']) # Nimble backend does not delete the snapshot from the parent volume # if there is a dependent clone. So the deletes need to be in reverse # order i.e. @@ -229,15 +208,17 @@ class NimbleBaseVolumeDriver(san.SanDriver): # a temporary operation in which we are certain that the snapshot does # not need to be preserved after the backup is completed. - if snap_name and vol_name: - self.APIExecutor.online_snap(vol_name, + if (backup_snap_name != "" and backup_vol_name != "") and ( + backup_snap_name is not None): + LOG.debug("Delete volume backup vol: %(vol)s snap: %(snap)s", + {'vol': backup_vol_name, + 'snap': backup_snap_name}) + self.APIExecutor.online_snap(backup_vol_name, False, - snap_name, - ignore_list=['SM-ealready', - 'SM-enoent']) - self.APIExecutor.delete_snap(vol_name, - snap_name, - ignore_list=['SM-enoent']) + backup_snap_name) + + self.APIExecutor.delete_snap(backup_vol_name, + backup_snap_name) def _generate_random_string(self, length): """Generates random_string.""" @@ -250,18 +231,19 @@ class NimbleBaseVolumeDriver(san.SanDriver): Extend the volume if the size of the volume is more than the snapshot. """ reserve = not self.configuration.san_thin_provision - self.APIExecutor.clone_vol(volume, snapshot, reserve) + self.APIExecutor.clone_vol(volume, snapshot, reserve, + self._group_target_enabled, + self._storage_protocol) if(volume['size'] > snapshot['volume_size']): - vol_size = volume['size'] * units.Gi - reserve_size = vol_size if reserve else 0 - self.APIExecutor.edit_vol( - volume['name'], - VOL_EDIT_MASK, # mask for vol attributes - {'size': vol_size, - 'reserve': reserve_size, - 'warn-level': int(vol_size * WARN_LEVEL), - 'quota': vol_size, - 'snap-quota': DEFAULT_SNAP_QUOTA}) + vol_size = volume['size'] * units.Ki + reserve_size = 100 if reserve else 0 + data = {"data": {'size': vol_size, + 'reserve': reserve_size, + 'warn_level': int(WARN_LEVEL), + 'limit': 100, + 'snap_limit': DEFAULT_SNAP_QUOTA}} + LOG.debug("Edit Vol request %(data)s", {'data': data}) + self.APIExecutor.edit_vol(volume['name'], data) return self._get_model_info(volume['name']) def create_cloned_volume(self, volume, src_vref): @@ -295,36 +277,69 @@ class NimbleBaseVolumeDriver(san.SanDriver): self.APIExecutor.online_snap( snapshot['volume_name'], False, - snapshot['name'], - ignore_list=['SM-ealready', 'SM-enoent']) + snapshot['name']) self.APIExecutor.delete_snap(snapshot['volume_name'], - snapshot['name'], - ignore_list=['SM-enoent']) + snapshot['name']) def create_volume_from_snapshot(self, volume, snapshot): """Create a volume from a snapshot.""" self._clone_volume_from_snapshot(volume, snapshot) return self._get_model_info(volume['name']) + def _enable_group_scoped_target(self, group_info): + if 'version_current' in group_info: + current_version = group_info['version_current'] + major_minor = current_version.split(".") + if len(major_minor) >= 3: + major = major_minor[0] + minor = major_minor[1] + # TODO(rkumar): Fix the major version + if int(major) >= 4 and int(minor) >= 0: + # Enforce group scoped target + if 'group_target_enabled' in group_info: + if group_info['group_target_enabled'] is False: + try: + self.APIExecutor.enable_group_scoped_target() + except Exception: + raise NimbleAPIException(_("Unable to enable" + " GST")) + self._group_target_enabled = True + LOG.info(_LI("Group Scoped Target enabled for " + "group %(group)s: %(ip)s"), + {'group': group_info['name'], + 'ip': self.configuration.san_ip}) + elif 'group_target_enabled' not in group_info: + LOG.info(_LI("Group Scoped Target NOT " + "present for group %(group)s: " + "%(ip)s"), + {'group': group_info['name'], + 'ip': self.configuration.san_ip}) + else: + raise NimbleAPIException(_("Unable to get current software " + "version for %s"), + self.configuration.san_ip) + def get_volume_stats(self, refresh=False): """Get volume stats. This is more of getting group stats.""" if refresh: - group_info = self.APIExecutor.get_group_config() - if not group_info['spaceInfoValid']: + group_info = self.APIExecutor.get_group_info() + if 'usage_valid' not in group_info: raise NimbleDriverException(_('SpaceInfo returned by' 'array is invalid')) - total_capacity = (group_info['usableCapacity'] / + total_capacity = (group_info['usable_capacity_bytes'] / float(units.Gi)) - used_space = ((group_info['volUsageCompressed'] + - group_info['snapUsageCompressed'] + - group_info['unusedReserve']) / + used_space = ((group_info['compressed_vol_usage_bytes'] + + group_info['compressed_snap_usage_bytes'] + + group_info['unused_reserve_bytes']) / float(units.Gi)) + free_space = total_capacity - used_space LOG.debug('total_capacity=%(capacity)f ' 'used_space=%(used)f free_space=%(free)f', {'capacity': total_capacity, 'used': used_space, 'free': free_space}) + backend_name = self.configuration.safe_get( 'volume_backend_name') or self.__class__.__name__ self.group_stats = {'volume_backend_name': backend_name, @@ -348,21 +363,19 @@ class NimbleBaseVolumeDriver(san.SanDriver): LOG.info(_LI('Entering extend_volume volume=%(vol)s ' 'new_size=%(size)s'), {'vol': volume_name, 'size': new_size}) - vol_size = int(new_size) * units.Gi + vol_size = int(new_size) * units.Ki reserve = not self.configuration.san_thin_provision - reserve_size = vol_size if reserve else 0 - self.APIExecutor.edit_vol( - volume_name, - VOL_EDIT_MASK, # mask for vol attributes - {'size': vol_size, - 'reserve': reserve_size, - 'warn-level': int(vol_size * WARN_LEVEL), - 'quota': vol_size, - 'snap-quota': DEFAULT_SNAP_QUOTA}) + reserve_size = 100 if reserve else 0 + LOG.debug("new volume size in MB (size)s", {'size': vol_size}) + data = {"data": {'size': vol_size, + 'reserve': reserve_size, + 'warn_level': int(WARN_LEVEL), + 'limit': 100, + 'snap_limit': DEFAULT_SNAP_QUOTA}} + self.APIExecutor.edit_vol(volume_name, data) def _get_existing_volume_ref_name(self, existing_ref): """Returns the volume name of an existing ref""" - vol_name = None if 'source-name' in existing_ref: vol_name = existing_ref['source-name'] @@ -380,39 +393,48 @@ class NimbleBaseVolumeDriver(san.SanDriver): # Get the volume name from the external reference target_vol_name = self._get_existing_volume_ref_name(external_ref) LOG.debug('Entering manage_existing. ' - 'Target_volume_name = %s', target_vol_name) + 'Target_volume_name =%s', target_vol_name) # Get vol info from the volume name obtained from the reference vol_info = self.APIExecutor.get_vol_info(target_vol_name) # Check if volume is already managed by OpenStack - if vol_info['agent-type'] == AGENT_TYPE_OPENSTACK: - msg = (_('Volume %s is already managed by OpenStack.') - % target_vol_name) + if vol_info['agent_type'] == AGENT_TYPE_OPENSTACK or ( + vol_info['agent_type'] == AGENT_TYPE_OPENSTACK_GST): raise exception.ManageExistingAlreadyManaged( volume_ref=volume['id']) # If agent-type is not None then raise exception - if vol_info['agent-type'] != AGENT_TYPE_NONE: + if vol_info['agent_type'] != AGENT_TYPE_NONE: msg = (_('Volume should have agent-type set as None.')) raise exception.InvalidVolume(reason=msg) new_vol_name = volume['name'] - - if vol_info['online']: + LOG.info(_LI("Volume status before managing it : %(status)s"), + {'status': vol_info['online']}) + if vol_info['online'] is True: msg = (_('Volume %s is online. Set volume to offline for ' 'managing using OpenStack.') % target_vol_name) raise exception.InvalidVolume(reason=msg) # edit the volume - self.APIExecutor.edit_vol(target_vol_name, - MANAGE_EDIT_MASK, - {'name': new_vol_name, - 'agent-type': AGENT_TYPE_OPENSTACK}) + data = {'data': {'name': new_vol_name}} + if self._group_target_enabled is True: + # check if any ACL's are attached to this volume + if 'access_control_records' in vol_info and ( + vol_info['access_control_records'] is not None): + msg = (_('Volume %s has ACL associated with it. Remove ACL ' + 'for managing using Openstack') % target_vol_name) + raise exception.InvalidVolume(reason=msg) + data['data']['agent_type'] = AGENT_TYPE_OPENSTACK_GST + else: + data['data']['agent_type'] = AGENT_TYPE_OPENSTACK + + LOG.debug("Data for edit %(data)s", {'data': data}) + self.APIExecutor.edit_vol(target_vol_name, data) # make the volume online after rename - self.APIExecutor.online_vol(new_vol_name, True, ignore_list=[ - 'SM-enoent']) + self.APIExecutor.online_vol(new_vol_name, True) return self._get_model_info(new_vol_name) @@ -430,35 +452,157 @@ class NimbleBaseVolumeDriver(san.SanDriver): LOG.debug('Volume size : %(size)s Volume-name : %(name)s', {'size': vol_info['size'], 'name': vol_info['name']}) - return int(math.ceil(float(vol_info['size']) / units.Gi)) + return int(vol_info['size'] / units.Ki) def unmanage(self, volume): """Removes the specified volume from Cinder management.""" vol_name = volume['name'] - LOG.info(_LI("Entering unmanage_volume volume = %s"), vol_name) + LOG.debug("Entering unmanage_volume volume =%s", vol_name) # check agent type vol_info = self.APIExecutor.get_vol_info(vol_name) - if vol_info['agent-type'] != AGENT_TYPE_OPENSTACK: + if vol_info['agent_type'] != AGENT_TYPE_OPENSTACK and ( + vol_info['agent_type'] != AGENT_TYPE_OPENSTACK_GST): msg = (_('Only volumes managed by OpenStack can be unmanaged.')) raise exception.InvalidVolume(reason=msg) + data = {'data': {'agent_type': AGENT_TYPE_NONE}} # update the agent-type to None - self.APIExecutor.edit_vol(vol_name, - UNMANAGE_EDIT_MASK, - {'agent-type': AGENT_TYPE_NONE}) + self.APIExecutor.edit_vol(vol_name, data) # offline the volume - self.APIExecutor.online_vol(vol_name, False, ignore_list=[ - 'SM-enoent']) + self.APIExecutor.online_vol(vol_name, False) + + def do_setup(self, context): + """Setup the Nimble Cinder volume driver.""" + self._check_config() + # Setup API Executor + try: + self.APIExecutor = NimbleRestAPIExecutor( + username=self.configuration.san_login, + password=self.configuration.san_password, + ip=self.configuration.san_ip, + verify=self.verify) + if self._storage_protocol == "iSCSI": + group_info = self.APIExecutor.get_group_info() + self._enable_group_scoped_target(group_info) + except Exception: + LOG.error(_LE('Failed to create REST client. ' + 'Check san_ip, username, password' + ' and make sure the array version is compatible')) + raise + self._update_existing_vols_agent_type(context) + + def _update_existing_vols_agent_type(self, context): + backend_name = self.configuration.safe_get('volume_backend_name') + all_vols = volume.VolumeList.get_all( + context, None, None, None, None, {'status': 'available'}) + for vol in all_vols: + if backend_name in vol.host: + try: + vol_info = self.APIExecutor.get_vol_info(vol.name) + # update agent_type only if no ACL's are present + if 'access_control_records' in vol_info and ( + vol_info['access_control_records'] is None): + if self._group_target_enabled: + LOG.debug("Updating %(vol)s to have agent_type :" + "%(agent)s", + {'vol': vol.name, + 'agent': AGENT_TYPE_OPENSTACK_GST}) + # check if this is an upgrade case from + # openstack to openstackv2 + if vol_info['agent_type'] == AGENT_TYPE_NONE: + data = {'data': {'agent_type': + AGENT_TYPE_OPENSTACK_GST}} + self.APIExecutor.edit_vol(vol.name, data) + elif vol_info['agent_type'] == ( + AGENT_TYPE_OPENSTACK): + # 1. update the agent type to None + data = {'data': {'agent_type': + AGENT_TYPE_NONE}} + self.APIExecutor.edit_vol(vol.name, data) + # 2. update the agent type to openstack_gst + data = {'data': {'agent_type': + AGENT_TYPE_OPENSTACK_GST}} + self.APIExecutor.edit_vol(vol.name, data) + else: + LOG.debug("Updating %(vol)s to have agent_type :" + "%(agent)s", + {'vol': vol.name, + 'agent': AGENT_TYPE_OPENSTACK_GST}) + if vol_info['agent_type'] == AGENT_TYPE_NONE: + data = {'data': {'agent_type': + AGENT_TYPE_OPENSTACK}} + self.APIExecutor.edit_vol(vol.name, data) + elif vol_info['agent_type'] == ( + AGENT_TYPE_OPENSTACK_GST): + # 1. update the agent type to None + data = {'data': {'agent_type': + AGENT_TYPE_NONE}} + self.APIExecutor.edit_vol(vol.name, data) + # 2. update the agent type to openstack + data = {'data': {'agent_type': + AGENT_TYPE_OPENSTACK}} + self.APIExecutor.edit_vol(vol.name, data) + except NimbleAPIException: + LOG.warning(_LW('Error updating agent-type for ' + 'volume %s.'), vol.name) + raise + + def _get_model_info(self, volume_name): + """Get model info for the volume.""" + return ( + {'provider_location': self._get_provider_location(volume_name), + 'provider_auth': None}) + + @abc.abstractmethod + def _get_provider_location(self, volume_name): + """Volume info for iSCSI and FC""" + + pass + + def _create_igroup_for_initiator(self, initiator_name, wwpn): + """Creates igroup for an initiator and returns the igroup name.""" + igrp_name = 'openstack-' + self._generate_random_string(12) + LOG.info(_LI('Creating initiator group %(grp)s ' + 'with initiator %(iname)s'), + {'grp': igrp_name, 'iname': initiator_name}) + if self._storage_protocol == "iSCSI": + self.APIExecutor.create_initiator_group(igrp_name) + self.APIExecutor.add_initiator_to_igroup(igrp_name, initiator_name) + elif self._storage_protocol == "FC": + self.APIExecutor.create_initiator_group_fc(igrp_name, wwpn) + return igrp_name + + def _get_igroupname_for_initiator_fc(self, initiator_wwpns): + initiator_groups = self.APIExecutor.get_initiator_grp_list() + for initiator_group in initiator_groups: + if 'fc_initiators' in initiator_group and initiator_group[ + 'fc_initiators'] is not None: + wwpns_list = [] + for initiator in initiator_group['fc_initiators']: + wwpn = str(initiator['wwpn']).replace(":", "") + wwpns_list.append(wwpn) + LOG.debug("initiator_wwpns=%(initiator)s " + "wwpns_list_from_array=%(wwpns)s" % + {'initiator': initiator_wwpns, + 'wwpns': wwpns_list}) + if set(initiator_wwpns) == set(wwpns_list): + LOG.info(_LI('igroup %(grp)s found for ' + 'initiator %(wwpns_list)s'), + {'grp': initiator_group['name'], + 'wwpns_list': wwpns_list}) + return initiator_group['name'] + LOG.info(_LI('No igroup found for initiators %s'), initiator_wwpns) + return '' def _get_igroupname_for_initiator(self, initiator_name): initiator_groups = self.APIExecutor.get_initiator_grp_list() for initiator_group in initiator_groups: - if 'initiator-list' in initiator_group: - if (len(initiator_group['initiator-list']) == 1 and - initiator_group['initiator-list'][0]['name'] == + if initiator_group['iscsi_initiators'] is not None: + if (len(initiator_group['iscsi_initiators']) == 1 and + initiator_group['iscsi_initiators'][0]['iqn'] == initiator_name): LOG.info(_LI('igroup %(grp)s found for ' 'initiator %(iname)s'), @@ -468,50 +612,38 @@ class NimbleBaseVolumeDriver(san.SanDriver): LOG.info(_LI('No igroup found for initiator %s'), initiator_name) return '' + def get_lun_number(self, volume, initiator_group_name): + vol_info = self.APIExecutor.get_vol_info(volume['name']) + for acl in vol_info['access_control_records']: + if (initiator_group_name == acl['initiator_group_name']): + LOG.info(_LI("access_control_record =%(acl)s"), + {'acl': acl}) + lun = acl['lun'] + LOG.info(_LI("LUN : %(lun)s"), {"lun": lun}) + return lun + raise NimbleAPIException(_("Lun number not found for volume %(vol)s " + "with initiator_group: %(igroup)s") % + {'vol': volume['name'], + 'igroup': initiator_group_name}) + @interface.volumedriver class NimbleISCSIDriver(NimbleBaseVolumeDriver, san.SanISCSIDriver): + """OpenStack driver to enable Nimble ISCSI Controller.""" def __init__(self, *args, **kwargs): super(NimbleISCSIDriver, self).__init__(*args, **kwargs) self._storage_protocol = "iSCSI" + self._group_target_name = None - def _get_discovery_ip(self, netconfig): - """Get discovery ip.""" - subnet_label = self.configuration.nimble_subnet_label - LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s', - {'netlabel': subnet_label, 'netconf': netconfig}) - ret_discovery_ip = '' - for subnet in netconfig['subnet-list']: - LOG.info(_LI('Exploring array subnet label %s'), subnet['label']) - if subnet_label == '*': - # Use the first data subnet, save mgmt+data for later - if subnet['subnet-id']['type'] == SM_SUBNET_DATA: - LOG.info(_LI('Discovery ip %(disc_ip)s is used ' - 'on data subnet %(net_label)s'), - {'disc_ip': subnet['discovery-ip'], - 'net_label': subnet['label']}) - return subnet['discovery-ip'] - elif (subnet['subnet-id']['type'] == - SM_SUBNET_MGMT_PLUS_DATA): - LOG.info(_LI('Discovery ip %(disc_ip)s is found' - ' on mgmt+data subnet %(net_label)s'), - {'disc_ip': subnet['discovery-ip'], - 'net_label': subnet['label']}) - ret_discovery_ip = subnet['discovery-ip'] - # If subnet is specified and found, use the subnet - elif subnet_label == subnet['label']: - LOG.info(_LI('Discovery ip %(disc_ip)s is used' - ' on subnet %(net_label)s'), - {'disc_ip': subnet['discovery-ip'], - 'net_label': subnet['label']}) - return subnet['discovery-ip'] - if ret_discovery_ip: - LOG.info(_LI('Discovery ip %s is used on mgmt+data subnet'), - ret_discovery_ip) - return ret_discovery_ip - raise NimbleDriverException(_('No suitable discovery ip found')) + def _set_gst_for_group(self): + group_info = self.APIExecutor.get_group_info() + if 'group_target_enabled' in group_info and ( + group_info['group_target_enabled']) is True and ( + 'group_target_name' in group_info) and ( + group_info['group_target_name'] is not None): + self._group_target_name = group_info['group_target_name'] def initialize_connection(self, volume, connector): """Driver entry point to attach a volume to an instance.""" @@ -524,19 +656,24 @@ class NimbleISCSIDriver(NimbleBaseVolumeDriver, san.SanISCSIDriver): initiator_group_name = self._get_igroupname_for_initiator( initiator_name) if not initiator_group_name: - initiator_group_name = self._create_igroup_for_initiator_iscsi( - initiator_name) + initiator_group_name = self._create_igroup_for_initiator( + initiator_name, None) LOG.info(_LI('Initiator group name is %(grp)s for initiator ' '%(iname)s'), {'grp': initiator_group_name, 'iname': initiator_name}) self.APIExecutor.add_acl(volume, initiator_group_name) - (iscsi_portal, iqn, lun_num) = volume['provider_location'].split() + (iscsi_portal, iqn) = volume['provider_location'].split() + if self._group_target_name is not None: + lun_num = self.get_lun_number(volume, initiator_group_name) + else: + lun_num = LUN_ID + properties = {} - properties['target_discovered'] = False # whether discovery was used properties['target_portal'] = iscsi_portal properties['target_iqn'] = iqn properties['target_lun'] = int(lun_num) properties['volume_id'] = volume['id'] # used by xen currently + return { 'driver_volume_type': 'iscsi', 'data': properties, @@ -546,39 +683,103 @@ class NimbleISCSIDriver(NimbleBaseVolumeDriver, san.SanISCSIDriver): """Driver entry point to unattach a volume from an instance.""" LOG.info(_LI('Entering terminate_connection volume=%(vol)s' ' connector=%(conn)s location=%(loc)s.'), - {'vol': volume, + {'vol': volume['name'], 'conn': connector, 'loc': volume['provider_location']}) initiator_name = connector['initiator'] initiator_group_name = self._get_igroupname_for_initiator( initiator_name) if not initiator_group_name: - raise NimbleDriverException( - _('No initiator group found for initiator %s') % - initiator_name) + raise NimbleDriverException(_('No initiator group found for ' + 'initiator %s') % initiator_name) self.APIExecutor.remove_acl(volume, initiator_group_name) - - def _create_igroup_for_initiator_iscsi(self, initiator_name): - """Creates igroup for an initiator and returns the igroup name.""" - igrp_name = 'openstack-' + self._generate_random_string(12) - LOG.info(_LI('Creating initiator group %(grp)s ' - 'with initiator %(iname)s'), - {'grp': igrp_name, 'iname': initiator_name}) - self.APIExecutor.create_initiator_group(igrp_name, initiator_name) - return igrp_name + eventlet.sleep(DEFAULT_SLEEP) def _get_provider_location(self, volume_name): """Get volume iqn for initiator access.""" vol_info = self.APIExecutor.get_vol_info(volume_name) - iqn = vol_info['target-name'] netconfig = self.APIExecutor.get_netconfig('active') - target_ipaddr = self._get_discovery_ip(netconfig) - iscsi_portal = target_ipaddr + ISCSI_TARGET_PORT - provider_location = '%s %s %s' % (iscsi_portal, iqn, LUN_ID) + + self._set_gst_for_group() + if self._group_target_name: + iqn = self._group_target_name + target_ipaddr = self._get_data_ip(netconfig) + iscsi_portal = target_ipaddr + ':3260' + else: + iqn = vol_info['target_name'] + target_ipaddr = self._get_discovery_ip(netconfig) + iscsi_portal = target_ipaddr + ':3260' + provider_location = '%s %s' % (iscsi_portal, iqn) LOG.info(_LI('vol_name=%(name)s provider_location=%(loc)s'), {'name': volume_name, 'loc': provider_location}) return provider_location + def _get_data_ip(self, netconfig): + """Get data ip.""" + subnet_label = self.configuration.nimble_subnet_label + LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s' % + {'netlabel': subnet_label, 'netconf': netconfig}) + ret_data_ip = '' + for subnet in netconfig['array_list'][0]['nic_list']: + LOG.info(_LI('Exploring array subnet label %s'), subnet[ + 'subnet_label']) + if subnet['data_ip']: + if subnet_label == '*': + # Use the first data subnet, save mgmt+data for later + LOG.info(_LI('Data ip %(data_ip)s is used ' + 'on data subnet %(net_label)s'), + {'data_ip': subnet['data_ip'], + 'net_label': subnet['subnet_label']}) + return subnet['data_ip'] + elif subnet_label == subnet['subnet_label']: + LOG.info(_LI('Data ip %(data_ip)s is used' + ' on subnet %(net_label)s'), + {'data_ip': subnet['data_ip'], + 'net_label': subnet['subnet_label']}) + return subnet['data_ip'] + if ret_data_ip: + LOG.info(_LI('Data ip %s is used on mgmt+data subnet'), + ret_data_ip) + return ret_data_ip + else: + raise NimbleDriverException(_('No suitable data ip found')) + + def _get_discovery_ip(self, netconfig): + """Get discovery ip.""" + subnet_label = self.configuration.nimble_subnet_label + LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s', + {'netlabel': subnet_label, 'netconf': netconfig}) + ret_discovery_ip = '' + for subnet in netconfig['subnet_list']: + LOG.info(_LI('Exploring array subnet label %s'), subnet['label']) + if subnet_label == '*': + # Use the first data subnet, save mgmt+data for later + if subnet['type'] == SM_SUBNET_DATA: + LOG.info(_LI('Discovery ip %(disc_ip)s is used ' + 'on data subnet %(net_label)s'), + {'disc_ip': subnet['discovery_ip'], + 'net_label': subnet['label']}) + return subnet['discovery_ip'] + elif (subnet['type'] == SM_SUBNET_MGMT_PLUS_DATA): + LOG.info(_LI('Discovery ip %(disc_ip)s is found' + ' on mgmt+data subnet %(net_label)s'), + {'disc_ip': subnet['discovery_ip'], + 'net_label': subnet['label']}) + ret_discovery_ip = subnet['discovery_ip'] + # If subnet is specified and found, use the subnet + elif subnet_label == subnet['label']: + LOG.info(_LI('Discovery ip %(disc_ip)s is used' + ' on subnet %(net_label)s'), + {'disc_ip': subnet['discovery_ip'], + 'net_label': subnet['label']}) + return subnet['discovery_ip'] + if ret_discovery_ip: + LOG.info(_LI('Discovery ip %s is used on mgmt+data subnet'), + ret_discovery_ip) + return ret_discovery_ip + else: + raise NimbleDriverException(_('No suitable discovery ip found')) + @interface.volumedriver class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver): @@ -592,7 +793,7 @@ class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver): def _get_provider_location(self, volume_name): """Get array info wwn details.""" netconfig = self.APIExecutor.get_netconfig('active') - array_name = netconfig['array-list'][0]['array-name'] + array_name = netconfig['group_leader_array'] provider_location = '%s' % (array_name) LOG.info(_LI('vol_name=%(name)s provider_location=%(loc)s'), {'name': volume_name, 'loc': provider_location}) @@ -600,6 +801,8 @@ class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver): def _build_initiator_target_map(self, target_wwns, connector): """Build the target_wwns and the initiator target map.""" + LOG.debug("_build_initiator_target_map for %(wwns)s" % + {'wwns': target_wwns}) init_targ_map = {} if self._lookup_service: @@ -639,17 +842,16 @@ class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver): initiator_group_name = self._get_igroupname_for_initiator_fc(wwpns) if not initiator_group_name: - initiator_group_name = self._create_igroup_for_initiator_fc( + initiator_group_name = self._create_igroup_for_initiator( initiator_name, wwpns) LOG.info(_LI('Initiator group name is %(grp)s for initiator ' '%(iname)s'), {'grp': initiator_group_name, 'iname': initiator_name}) self.APIExecutor.add_acl(volume, initiator_group_name) - lun = self.get_lun_number(volume, initiator_group_name) init_targ_map = {} - array_name = volume['provider_location'].split() + (array_name) = volume['provider_location'].split() target_wwns = self.get_wwpns_from_array(array_name) @@ -681,7 +883,7 @@ class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver): for wwpn in connector['wwpns']: wwpns.append(wwpn) init_targ_map = {} - array_name = volume['provider_location'].split() + (array_name) = volume['provider_location'].split() target_wwns = self.get_wwpns_from_array(array_name) init_targ_map = self._build_initiator_target_map(target_wwns, connector) @@ -690,88 +892,33 @@ class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver): raise NimbleDriverException( _('No initiator group found for initiator %s') % initiator_name) - LOG.debug("initiator_target_map".format(init_targ_map)) + LOG.debug("initiator_target_map %s" % init_targ_map) self.APIExecutor.remove_acl(volume, initiator_group_name) + eventlet.sleep(DEFAULT_SLEEP) # FIXME to check for other volumes attached to the host and then # return the data. Bug https://bugs.launchpad.net/cinder/+bug/1617472 data = {'driver_volume_type': 'fibre_channel', - 'data': {}} + 'data': {'target_wwn': target_wwns}} return data - def _get_igroupname_for_initiator_fc(self, initiator_wwpns): - initiator_groups = self.APIExecutor.get_initiator_grp_list() - for initiator_group in initiator_groups: - if 'initiator-list' in initiator_group: - wwpns_list = [] - for initiator in initiator_group['initiator-list']: - wwpn = str(initiator['wwpn']).replace(":", "") - wwpns_list.append(wwpn) - LOG.debug("initiator_wwpns= %s wwpns_list_from_array=%s", - initiator_wwpns, wwpns_list) - if set(initiator_wwpns) == set(wwpns_list): - LOG.info(_LI('igroup %(grp)s found for ' - 'initiator %(wwpns_list)s'), - {'grp': initiator_group['name'], - 'wwpns_list': wwpns_list}) - return initiator_group['name'] - LOG.info(_LI('No igroup found for initiator %s'), initiator_wwpns) - return '' - - def get_lun_number(self, volume, initiator_group_name): - vol_info = self.APIExecutor.get_vol_info(volume['name']) - for acl in vol_info['aclList']: - if (initiator_group_name == acl['initiatorgrp']): - LOG.info(_LI("acllist =%(aclList)s"), - {'aclList': vol_info['aclList']}) - lun = vol_info['aclList'][0]['lun'] - return lun - - def _create_igroup_for_initiator_fc(self, initiator_name, wwpn_list): - """Creates igroup for an initiator and returns the igroup name.""" - igrp_name = 'openstack-' + self._generate_random_string(12) - LOG.info(_LI('Creating initiator group %(grp)s ' - 'with initiator %(iname)s'), - {'grp': igrp_name, 'iname': initiator_name}) - self.APIExecutor.create_initiator_group_fc(igrp_name, initiator_name, - wwpn_list) - return igrp_name - def get_wwpns_from_array(self, array_name): """Retrieve the wwpns from the array""" + LOG.debug("get_wwpns_from_array %s" % array_name) target_wwpns = [] interface_info = self.APIExecutor.get_fc_interface_list(array_name) - - for wwpn_list in interface_info['ctrlr-a-interface-list']: - wwpn = wwpn_list['wwpn'] - wwpn = wwpn.replace(":", "") - target_wwpns.append(wwpn) - - for wwpn_list in interface_info['ctrlr-b-interface-list']: + LOG.info(_LI("interface_info %(interface_info)s"), + {"interface_info": interface_info}) + for wwpn_list in interface_info: wwpn = wwpn_list['wwpn'] wwpn = wwpn.replace(":", "") target_wwpns.append(wwpn) return target_wwpns - -def _response_checker(func): - """Decorator function to check if the response of an API is positive.""" - @functools.wraps(func) - def inner_response_checker(self, *args, **kwargs): - response = func(self, *args, **kwargs) - ignore_list = (kwargs['ignore_list'] - if 'ignore_list' in kwargs else []) - for err in response['err-list']['err-list']: - err_str = self._get_err_str(err['code']) - if err_str != 'SM-ok' and err_str not in ignore_list: - msg = (_('API %(name)s failed with error string %(err)s') - % {'name': func.__name__, 'err': err_str}) - LOG.error(msg) - raise NimbleAPIException(msg) - return response - return inner_response_checker + def _convert_string_to_colon_separated_wwnn(self, wwnn): + return ':'.join(a + b for a, b in zip(wwnn[::2], wwnn[1::2])) def _connection_checker(func): @@ -781,8 +928,8 @@ def _connection_checker(func): for attempts in range(2): try: return func(self, *args, **kwargs) - except NimbleAPIException as e: - if attempts < 1 and (re.search('SM-eaccess', + except Exception as e: + if attempts < 1 and (re.search("Failed to execute", six.text_type(e))): LOG.info(_LI('Session might have expired.' ' Trying to relogin')) @@ -794,72 +941,90 @@ def _connection_checker(func): return inner_connection_checker -class NimbleAPIExecutor(object): +class NimbleRestAPIExecutor(object): - """Makes Nimble API calls.""" + """Makes Nimble REST API calls.""" - def __init__(self, *args, **kwargs): - self.sid = None + def __init__(self, api_version=NimbleDefaultVersion, *args, **kwargs): + self.token_id = None + self.ip = kwargs['ip'] self.username = kwargs['username'] self.password = kwargs['password'] - self.ip = kwargs['ip'] - - wsdl_url = 'https://%s/wsdl/NsGroupManagement.wsdl' % (kwargs['ip']) - LOG.debug('Using Nimble wsdl_url: %s', wsdl_url) - self.err_string_dict = self._create_err_code_to_str_mapper(wsdl_url) - self.client = client.Client(wsdl_url, - username=self.username, - password=self.password) - soap_url = ('https://%(ip)s:%(port)s/soap' % {'ip': kwargs['ip'], - 'port': SOAP_PORT}) - LOG.debug('Using Nimble soap_url: %s', soap_url) - self.client.set_options(location=soap_url) + self.verify = kwargs['verify'] + self.api_version = api_version + self.uri = "https://%(ip)s:5392/v%(version)s/" % { + 'ip': self.ip, + 'version': self.api_version} self.login() - def _create_err_code_to_str_mapper(self, wsdl_url): - f = urllib.request.urlopen(wsdl_url) - wsdl_file = f.read() - err_enums = re.findall( - r'(.*?)', - wsdl_file, - re.DOTALL) - err_enums = ''.join(err_enums).split('\n') - ret_dict = {} - for enum in err_enums: - m = re.search(r'"(.*?)"(.*?)= (\d+) ', enum) - if m: - ret_dict[int(m.group(3))] = m.group(1) - return ret_dict - - def _get_err_str(self, code): - if code in self.err_string_dict: - return self.err_string_dict[code] - else: - return 'Unknown error Code: %s' % code - - @_response_checker - def _execute_login(self): - return self.client.service.login(req={ - 'username': self.username, - 'password': self.password - }) - def login(self): - """Execute Https Login API.""" - response = self._execute_login() - LOG.info(_LI('Successful login by user %s'), self.username) - self.sid = response['authInfo']['sid'] + data = {'data': {"username": self.username, + "password": self.password, + "app_name": "NimbleCinderDriver"}} + r = requests.post(self.uri + "tokens", + data=json.dumps(data), + verify=self.verify) - @_connection_checker - @_response_checker - def _execute_get_netconfig(self, name): - return self.client.service.getNetConfig(request={'sid': self.sid, - 'name': name}) + if r.status_code != 201 and r.status_code != 200: + msg = _("Failed to login for user %s"), self.username + raise NimbleAPIException(msg) + self.token_id = r.json()['data']['session_token'] + self.headers = {'X-Auth-Token': self.token_id} - def get_netconfig(self, name): - """Execute getNetConfig API.""" - response = self._execute_get_netconfig(name) - return response['config'] + def get_group_id(self): + api = 'groups' + r = self.get(api) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve Group Object for : " + "%s") % self.ip) + return r.json()['data'][0]['id'] + + def get_group_info(self): + group_id = self.get_group_id() + api = 'groups/' + six.text_type(group_id) + r = self.get(api) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve Group info for: %s") + % group_id) + return r.json()['data'] + + def get_folder_id(self, folder_name): + api = 'folders' + filter = {"name": folder_name} + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve information for " + "Folder: %s") % folder_name) + return r.json()['data'][0]['id'] + + def get_folder_info(self, folder_name): + folder_id = self.get_folder_id(folder_name) + api = "folders/" + six.text_type(folder_id) + r = self.get(api) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve Folder info for: " + "%s") % folder_id) + return r.json()['data'] + + def get_performance_policy_id(self, perf_policy_name): + api = 'performance_policies/' + filter = {'name': perf_policy_name} + LOG.debug("Perfomance policy Name %s" % perf_policy_name) + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("No performance policy found for:" + "%(perf)s") % {'perf': perf_policy_name}) + LOG.debug("Performance policy ID :%(perf)s" % + {'perf': r.json()['data'][0]['id']}) + return r.json()['data'][0]['id'] + + def get_netconfig(self, role): + api = "network_configs/detail" + filter = {'role': role} + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("No %s network config exists") % role) + return r.json()['data'][0] def _get_volumetype_extraspecs(self, volume): specs = {} @@ -884,28 +1049,44 @@ class NimbleAPIExecutor(object): return extra_specs_map - @_connection_checker - @_response_checker - def _execute_create_vol(self, volume, pool_name, reserve): + def create_vol(self, volume, pool_name, reserve, protocol, is_gst_enabled): + response = self._execute_create_vol(volume, pool_name, reserve, + protocol, is_gst_enabled) + LOG.info(_LI('Successfully created volume %(name)s'), + {'name': response['name']}) + return response['name'] + + def _execute_create_vol(self, volume, pool_name, reserve, protocol, + is_gst_enabled): + """Create volume + + :return: r['data'] + """ + # Set volume size, display name and description - volume_size = volume['size'] * units.Gi - reserve_size = volume_size if reserve else 0 + volume_size = volume['size'] * units.Ki + reserve_size = 100 if reserve else 0 # Set volume description display_list = [getattr(volume, 'display_name', ''), getattr(volume, 'display_description', '')] description = ':'.join(filter(None, display_list)) # Limit description size to 254 characters description = description[:254] + pool_id = self.get_pool_id(pool_name) specs = self._get_volumetype_extraspecs(volume) extra_specs_map = self._get_extra_spec_values(specs) perf_policy_name = extra_specs_map[EXTRA_SPEC_PERF_POLICY] + perf_policy_id = self.get_performance_policy_id(perf_policy_name) encrypt = extra_specs_map[EXTRA_SPEC_ENCRYPTION] multi_initiator = extra_specs_map[EXTRA_SPEC_MULTI_INITIATOR] - # default value of cipher for encryption cipher = DEFAULT_CIPHER if encrypt.lower() == 'yes': cipher = AES_256_XTS_CIPHER + if is_gst_enabled is True: + agent_type = AGENT_TYPE_OPENSTACK_GST + else: + agent_type = AGENT_TYPE_OPENSTACK LOG.debug('Creating a new volume=%(vol)s size=%(size)s' ' reserve=%(reserve)s in pool=%(pool)s' @@ -916,210 +1097,296 @@ class NimbleAPIExecutor(object): ' multi-initiator=%(multi-initiator)s', {'vol': volume['name'], 'size': volume_size, - 'reserve': reserve, + 'reserve': reserve_size, 'pool': pool_name, 'description': description, 'perfpol-name': perf_policy_name, 'encryption': encrypt, 'cipher': cipher, - 'agent-type': AGENT_TYPE_OPENSTACK, + 'agent-type': agent_type, 'multi-initiator': multi_initiator}) + data = {"data": + {'name': volume['name'], + 'description': description, + 'size': volume_size, + 'reserve': reserve_size, + 'warn_level': int(WARN_LEVEL), + 'limit': 100, + 'snap_limit': DEFAULT_SNAP_QUOTA, + 'online': True, + 'pool_id': pool_id, + 'agent_type': agent_type, + 'perfpolicy_id': perf_policy_id, + 'encryption_cipher': cipher}} - return self.client.service.createVol( - request={'sid': self.sid, - 'attr': {'name': volume['name'], - 'description': description, - 'size': volume_size, - 'reserve': reserve_size, - 'warn-level': int(volume_size * WARN_LEVEL), - 'quota': volume_size, - 'snap-quota': DEFAULT_SNAP_QUOTA, - 'online': True, - 'pool-name': pool_name, - 'agent-type': AGENT_TYPE_OPENSTACK, - 'perfpol-name': perf_policy_name, - 'encryptionAttr': {'cipher': cipher}, - 'multi-initiator': multi_initiator}}) + if protocol == "iSCSI": + data['data']['multi_initiator'] = multi_initiator + api = 'volumes' + r = self.post(api, data) + return r['data'] - def create_vol(self, volume, pool_name, reserve): - """Execute createVol API.""" - response = self._execute_create_vol(volume, pool_name, reserve) - LOG.info(_LI('Successfully create volume %s'), response['name']) - return response['name'] + def create_initiator_group(self, initiator_grp_name): + api = "initiator_groups" + data = {"data": {"name": initiator_grp_name, + "access_protocol": "iscsi", + }} + r = self.post(api, data) + return r['data'] - @_connection_checker - @_response_checker - def _execute_get_group_config(self): - LOG.debug('Getting group config information') - return self.client.service.getGroupConfig(request={'sid': self.sid}) + def create_initiator_group_fc(self, initiator_grp_name, wwpns): + api = "initiator_groups" - def get_group_config(self): - """Execute getGroupConfig API.""" - response = self._execute_get_group_config() - LOG.debug('Successfully retrieved group config information') - return response['info'] + data = {} + data["data"] = {} + data["data"]["name"] = initiator_grp_name + data["data"]["access_protocol"] = "fc" + data["data"]["fc_initiators"] = [] + for wwpn in wwpns: + initiator = {} + initiator["wwpn"] = self._format_to_wwpn(wwpn) + data["data"]["fc_initiators"].append(initiator) + r = self.post(api, data) + return r['data'] + + def get_initiator_grp_id(self, initiator_grp_name): + api = "initiator_groups" + filter = {'name': initiator_grp_name} + r = self.get_query(api, filter) + return r.json()['data'][0]['id'] + + def add_initiator_to_igroup(self, initiator_grp_name, initiator_name): + initiator_group_id = self.get_initiator_grp_id(initiator_grp_name) + api = "initiators" + data = {"data": { + "access_protocol": "iscsi", + "initiator_group_id": initiator_group_id, + "label": initiator_name, + "iqn": initiator_name + }} + r = self.post(api, data) + return r['data'] + + def get_pool_id(self, pool_name): + api = "pools/" + filter = {'name': pool_name} + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve information for " + "pool : %(pool)s") % + {'pool': pool_name}) + return r.json()['data'][0]['id'] + + def get_initiator_grp_list(self): + api = "initiator_groups/detail" + r = self.get(api) + if 'data' not in r.json(): + raise NimbleAPIException(_("Unable to retrieve initiator group " + "list")) + LOG.info(_LI('Successfully retrieved InitiatorGrpList')) + return r.json()['data'] + + def get_initiator_grp_id_by_name(self, initiator_group_name): + api = 'initiator_groups' + filter = {"name": initiator_group_name} + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve information for" + "initiator group : %s") % + initiator_group_name) + return r.json()['data'][0]['id'] + + def get_volume_id_by_name(self, name): + api = "volumes" + filter = {"name": name} + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve information for " + "volume: %s") % name) + return r.json()['data'][0]['id'] + + def get_volume_name(self, volume_id): + api = "volumes/" + six.text_type(volume_id) + r = self.get(api) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve information for " + "volume: %s") % volume_id) + return r.json()['data']['name'] - @_connection_checker - @_response_checker def add_acl(self, volume, initiator_group_name): - """Execute addAcl API.""" - LOG.info(_LI('Adding ACL to volume=%(vol)s with' - ' initiator group name %(igrp)s'), - {'vol': volume['name'], - 'igrp': initiator_group_name}) - return self.client.service.addVolAcl( - request={'sid': self.sid, - 'volname': volume['name'], - 'apply-to': SM_ACL_APPLY_TO_BOTH, - 'chapuser': SM_ACL_CHAP_USER_ANY, - 'initiatorgrp': initiator_group_name}) + initiator_group_id = self.get_initiator_grp_id_by_name( + initiator_group_name) + volume_id = self.get_volume_id_by_name(volume['name']) + data = {'data': {"apply_to": 'both', + "initiator_group_id": initiator_group_id, + "vol_id": volume_id + }} + api = 'access_control_records' + r = self.post(api, data) + return r + + def get_acl_record(self, volume_id, initiator_group_id): + filter = {"vol_id": volume_id, + "initiator_group_id": initiator_group_id} + api = "access_control_records" + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve ACL for volume: " + "%(vol)s %(igroup)s ") % + {'vol': volume_id, + 'igroup': initiator_group_id}) + return r.json()['data'][0] - @_connection_checker - @_response_checker def remove_acl(self, volume, initiator_group_name): - """Execute removeVolAcl API.""" - LOG.info(_LI('Removing ACL from volume=%(vol)s' - ' for initiator group %(igrp)s'), - {'vol': volume['name'], - 'igrp': initiator_group_name}) - return self.client.service.removeVolAcl( - request={'sid': self.sid, - 'volname': volume['name'], - 'apply-to': SM_ACL_APPLY_TO_BOTH, - 'chapuser': SM_ACL_CHAP_USER_ANY, - 'initiatorgrp': initiator_group_name}) + LOG.info(_LI("removing ACL from volume=%(vol)s" + "and %(igroup)s"), + {"vol": volume['name'], + "igroup": initiator_group_name}) + initiator_group_id = self.get_initiator_grp_id_by_name( + initiator_group_name) + volume_id = self.get_volume_id_by_name(volume['name']) - @_connection_checker - @_response_checker - def _execute_get_vol_info(self, vol_name): - LOG.info(_LI('Getting volume information ' - 'for vol_name=%s'), vol_name) - return self.client.service.getVolInfo(request={'sid': self.sid, - 'name': vol_name}) + acl_record = self.get_acl_record(volume_id, initiator_group_id) + LOG.debug("ACL Record %(acl)s" % {"acl": acl_record}) + acl_id = acl_record['id'] + api = 'access_control_records/' + six.text_type(acl_id) + r = self.delete(api) + return r - def get_vol_info(self, vol_name): - """Execute getVolInfo API.""" - response = self._execute_get_vol_info(vol_name) - LOG.info(_LI('Successfully got volume information for volume %s'), - vol_name) - return response['vol'] - - @_connection_checker - @_response_checker - def _execute_get_snap_info(self, snap_name, vol_name): - LOG.info(_LI('Getting snapshot information for %(vol_name)s ' - '%(snap_name)s'), {'vol_name': vol_name, - 'snap_name': snap_name}) - return self.client.service.getSnapInfo(request={'sid': self.sid, - 'vol': vol_name, - 'name': snap_name}) + def get_snap_info_by_id(self, snap_id, vol_id): + filter = {"id": snap_id, "vol_id": vol_id} + api = 'snapshots' + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("Unable to retrieve snapshot info for " + "snap_id: %(snap)s volume id: %(vol)s") + % {'snap': snap_id, + 'vol': vol_id}) + LOG.debug("SnapInfo :%s" % six.text_type(r.json()['data'][0])) + return r.json()['data'][0] def get_snap_info(self, snap_name, vol_name): - """Get snapshot information. + filter = {"name": snap_name, "vol_name": vol_name} + api = 'snapshots' + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("Snapshot: %(snap)s of Volume: %(vol)s " + "doesn't exist") % + {'snap': snap_name, + 'vol': vol_name}) + return r.json()['data'][0] - :param snap_name: snapshot name - :param vol_name: volume name - :return: response object - """ + def get_snap_info_detail(self, snap_id): + api = 'snapshots/detail' + filter = {'id': snap_id} + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("Snapshot: %s doesnt exist") % snap_id) + return r.json()['data'][0] - response = self._execute_get_snap_info(snap_name, vol_name) - LOG.info(_LI('Successfully got snapshot information for snapshot ' - '%(snap)s and %(volume)s'), - {'snap': snap_name, - 'volume': vol_name}) - return response['snap'] + @utils.retry(NimbleAPIException, 2, 3) + def online_vol(self, volume_name, online_flag): + volume_id = self.get_volume_id_by_name(volume_name) + LOG.debug("volume_id %s", six.text_type(volume_id)) + eventlet.sleep(DEFAULT_SLEEP) + api = "volumes/" + six.text_type(volume_id) + data = {'data': {"online": online_flag}} + try: + self.put(api, data) + LOG.debug("Volume %(vol)s is in requested online state :%(flag)s" % + {'vol': volume_name, + 'flag': online_flag}) + except Exception as ex: + msg = (_("Error %s") % ex) + LOG.debug("online_vol_exception: %s" % msg) + if msg.__contains__("Object is %s" % SM_STATE_MSG): + LOG.warning(_LW('Volume %(vol)s : %(state)s'), + {'vol': volume_name, + 'state': SM_STATE_MSG}) + # TODO(rkumar): Check if we need to ignore the connected + # initiator + elif msg.__contains__("Initiators are connected to"): + raise NimbleAPIException(msg) + else: + raise exception.InvalidVolume(reason=msg) - @_connection_checker - @_response_checker - def online_vol(self, vol_name, online_flag, *args, **kwargs): - """Execute onlineVol API.""" - LOG.info(_LI('Setting volume %(vol)s to online_flag %(flag)s'), - {'vol': vol_name, 'flag': online_flag}) - return self.client.service.onlineVol(request={'sid': self.sid, - 'name': vol_name, - 'online': online_flag}) + def online_snap(self, volume_name, online_flag, snap_name): + snap_info = self.get_snap_info(snap_name, volume_name) + api = "snapshots/" + six.text_type(snap_info['id']) + data = {'data': {"online": online_flag}} + try: + self.put(api, data) + LOG.debug("Snapshot %(snap)s is in requested online state " + ":%(flag)s" % { + 'snap': snap_name, + 'flag': online_flag}) + except Exception as ex: + LOG.debug("online_snap_exception: %s" % ex) + if six.text_type(ex).__contains__("Object %s" % SM_STATE_MSG): + LOG.warning(_LW('Snapshot %(snap)s :%(state)s'), + {'snap': snap_name, + 'state': SM_STATE_MSG}) + else: + raise - @_connection_checker - @_response_checker - def online_snap(self, vol_name, online_flag, snap_name, *args, **kwargs): - """Execute onlineSnap API.""" - LOG.info(_LI('Setting snapshot %(snap)s to online_flag %(flag)s'), - {'snap': snap_name, 'flag': online_flag}) - return self.client.service.onlineSnap(request={'sid': self.sid, - 'vol': vol_name, - 'name': snap_name, - 'online': online_flag}) + @utils.retry(NimbleAPIException, 2, 3) + def get_vol_info(self, volume_name): + volume_id = self.get_volume_id_by_name(volume_name) + api = 'volumes/' + six.text_type(volume_id) + r = self.get(api) + if not r.json()['data']: + raise exception.VolumeNotFound(_("Volume: %s not found") % + volume_name) + return r.json()['data'] - @_connection_checker - @_response_checker - def dissociate_volcoll(self, vol_name, *args, **kwargs): - """Execute dissocProtPol API.""" - LOG.info(_LI('Dissociating volume %s '), vol_name) - return self.client.service.dissocProtPol( - request={'sid': self.sid, - 'vol-name': vol_name}) + def delete_vol(self, volume_name): + volume_id = self.get_volume_id_by_name(volume_name) + api = "volumes/" + six.text_type(volume_id) + self.delete(api) - @_connection_checker - @_response_checker - def delete_vol(self, vol_name, *args, **kwargs): - """Execute deleteVol API.""" - LOG.info(_LI('Deleting volume %s '), vol_name) - return self.client.service.deleteVol(request={'sid': self.sid, - 'name': vol_name}) - - @_connection_checker - @_response_checker def snap_vol(self, snapshot): - """Execute snapVol API.""" + api = "snapshots" volume_name = snapshot['volume_name'] + vol_id = self.get_volume_id_by_name(volume_name) snap_name = snapshot['name'] # Set snapshot description - display_list = [getattr(snapshot, 'display_name', snapshot[ - 'display_name']), - getattr(snapshot, 'display_description', '')] + display_list = [ + getattr(snapshot, 'display_name', snapshot['display_name']), + getattr(snapshot, 'display_description', '')] snap_description = ':'.join(filter(None, display_list)) # Limit to 254 characters - LOG.debug("snap_description %s", snap_description) snap_description = snap_description[:254] - LOG.info(_LI('Creating snapshot for volume_name=%(vol)s' - ' snap_name=%(name)s snap_description=%(desc)s'), - {'vol': volume_name, - 'name': snap_name, - 'desc': snap_description}) - return self.client.service.snapVol( - request={'sid': self.sid, - 'vol': volume_name, - 'snapAttr': {'name': snap_name, - 'description': snap_description}}) + data = {"data": {"name": snap_name, + "description": snap_description, + "vol_id": vol_id + } + } + r = self.post(api, data) + return r['data'] - @_connection_checker - @_response_checker - def delete_snap(self, vol_name, snap_name, *args, **kwargs): - """Execute deleteSnap API.""" - LOG.info(_LI('Deleting snapshot %s '), snap_name) - return self.client.service.deleteSnap(request={'sid': self.sid, - 'vol': vol_name, - 'name': snap_name}) - - @_connection_checker - @_response_checker - def clone_vol(self, volume, snapshot, reserve): - """Execute cloneVol API.""" + def clone_vol(self, volume, snapshot, reserve, is_gst_enabled, + protocol): + api = "volumes" volume_name = snapshot['volume_name'] snap_name = snapshot['name'] + snap_info = self.get_snap_info(snap_name, volume_name) clone_name = volume['name'] snap_size = snapshot['volume_size'] - reserve_size = snap_size * units.Gi if reserve else 0 + reserve_size = 100 if reserve else 0 specs = self._get_volumetype_extraspecs(volume) extra_specs_map = self._get_extra_spec_values(specs) perf_policy_name = extra_specs_map.get(EXTRA_SPEC_PERF_POLICY) + perf_policy_id = self.get_performance_policy_id(perf_policy_name) encrypt = extra_specs_map.get(EXTRA_SPEC_ENCRYPTION) multi_initiator = extra_specs_map.get(EXTRA_SPEC_MULTI_INITIATOR) # default value of cipher for encryption cipher = DEFAULT_CIPHER if encrypt.lower() == 'yes': cipher = AES_256_XTS_CIPHER + if is_gst_enabled is True: + agent_type = AGENT_TYPE_OPENSTACK_GST + else: + agent_type = AGENT_TYPE_OPENSTACK LOG.info(_LI('Cloning volume from snapshot volume=%(vol)s ' 'snapshot=%(snap)s clone=%(clone)s snap_size=%(size)s ' @@ -1131,102 +1398,113 @@ class NimbleAPIExecutor(object): 'snap': snap_name, 'clone': clone_name, 'size': snap_size, - 'reserve': reserve, - 'agent-type': AGENT_TYPE_OPENSTACK, + 'reserve': reserve_size, + 'agent-type': agent_type, 'perfpol-name': perf_policy_name, 'encryption': encrypt, 'cipher': cipher, 'multi-initiator': multi_initiator}) - clone_size = snap_size * units.Gi - return self.client.service.cloneVol( - request={'sid': self.sid, - 'name': volume_name, - 'attr': {'name': clone_name, - 'reserve': reserve_size, - 'warn-level': int(clone_size * WARN_LEVEL), - 'quota': clone_size, - 'snap-quota': DEFAULT_SNAP_QUOTA, - 'online': True, - 'agent-type': AGENT_TYPE_OPENSTACK, - 'perfpol-name': perf_policy_name, - 'encryptionAttr': {'cipher': cipher}, - 'multi-initiator': multi_initiator}, - 'snap-name': snap_name}) + + data = {"data": {"name": clone_name, + "clone": 'true', + "base_snap_id": snap_info['id'], + 'snap_limit': DEFAULT_SNAP_QUOTA, + 'warn_level': int(WARN_LEVEL), + 'limit': 100, + "online": 'true', + "reserve": reserve_size, + "agent_type": agent_type, + "perfpolicy_id": perf_policy_id, + "encryption_cipher": cipher + } + } + if protocol == "iSCSI": + data['data']['multi_initiator'] = multi_initiator + + r = self.post(api, data) + return r['data'] + + def edit_vol(self, volume_name, data): + vol_id = self.get_volume_id_by_name(volume_name) + api = "volumes/" + six.text_type(vol_id) + self.put(api, data) + + def delete_snap(self, volume_name, snap_name): + snap_info = self.get_snap_info(snap_name, volume_name) + api = "snapshots/" + six.text_type(snap_info['id']) + self.delete(api) @_connection_checker - @_response_checker - def edit_vol(self, vol_name, mask, attr): - """Execute editVol API.""" - LOG.info(_LI('Editing Volume %(vol)s with mask %(mask)s'), - {'vol': vol_name, 'mask': str(mask)}) - return self.client.service.editVol(request={'sid': self.sid, - 'name': vol_name, - 'mask': mask, - 'attr': attr}) + def get(self, api): + return self.get_query(api, None) @_connection_checker - @_response_checker - def _execute_get_initiator_grp_list(self): - LOG.info(_LI('Getting getInitiatorGrpList')) - return (self.client.service.getInitiatorGrpList( - request={'sid': self.sid})) - - def get_initiator_grp_list(self): - """Execute getInitiatorGrpList API.""" - response = self._execute_get_initiator_grp_list() - LOG.info(_LI('Successfully retrieved InitiatorGrpList')) - return (response['initiatorgrp-list'] - if 'initiatorgrp-list' in response else []) + def get_query(self, api, query): + url = self.uri + api + return requests.get(url, headers=self.headers, + params=query, verify=False) @_connection_checker - @_response_checker - def create_initiator_group_fc(self, initiator_group_name, initiator_name, - wwpn_list): - """Execute createInitiatorGrp API for Fibre Channel""" - LOG.info(_LI('Creating initiator group %(igrp)s' - ' with wwpn %(wwpn)s'), - {'igrp': initiator_group_name, 'wwpn': wwpn_list}) - request = {} - request['sid'] = self.sid - request['attr'] = {} - request['attr']['name'] = initiator_group_name - request['attr']['access-protocol'] = FIBRE_CHANNEL_PROTOCOL - request['attr']['initiator-list'] = [] - for wwpn in wwpn_list: - initiator = {} - initiator['access-protocol'] = FIBRE_CHANNEL_PROTOCOL - initiator['wwpn'] = wwpn - request['attr']['initiator-list'].append(initiator) - LOG.debug("createInitiatorGrp request %s", request) - - return self.client.service.createInitiatorGrp(request=request) + def put(self, api, payload): + url = self.uri + api + r = requests.put(url, data=json.dumps(payload), + headers=self.headers, verify=self.verify) + if r.status_code != 201 and r.status_code != 200: + base = "Failed to execute api %(api)s : Error Code :%(code)s" % { + 'api': api, + 'code': r.status_code} + LOG.debug("Base error : %(base)s", {'base': base}) + try: + msg = _("%(base)s Message: %(msg)s") % { + 'base': base, + 'msg': r.json()['messages'][1]['text']} + except IndexError: + msg = _("%(base)s Message: %(msg)s") % { + 'base': base, + 'msg': six.text_type(r.json())} + raise NimbleAPIException(msg) + return r.json() @_connection_checker - @_response_checker - def create_initiator_group(self, initiator_group_name, initiator_name): - """Execute createInitiatorGrp API.""" - LOG.info(_LI('Creating initiator group %(igrp)s' - ' with one initiator %(iname)s'), - {'igrp': initiator_group_name, 'iname': initiator_name}) - return self.client.service.createInitiatorGrp( - request={'sid': self.sid, - 'attr': {'name': initiator_group_name, - 'initiator-list': [{'label': initiator_name, - 'name': initiator_name}]}}) + def post(self, api, payload): + url = self.uri + api + r = requests.post(url, data=json.dumps(payload), + headers=self.headers, verify=self.verify) + if r.status_code != 201 and r.status_code != 200: + msg = _("Failed to execute api %(api)s : %(msg)s : %(code)s") % { + 'api': api, + 'msg': r.json()['messages'][1]['text'], + 'code': r.status_code} + raise NimbleAPIException(msg) + return r.json() @_connection_checker - @_response_checker - def delete_initiator_group(self, initiator_group_name, *args, **kwargs): - """Execute deleteInitiatorGrp API.""" - LOG.info(_LI('Deleting deleteInitiatorGrp %s '), initiator_group_name) - return self.client.service.deleteInitiatorGrp( - request={'sid': self.sid, - 'name': initiator_group_name}) + def delete(self, api): + url = self.uri + api + r = requests.delete(url, headers=self.headers, verify=self.verify) + if r.status_code != 201 and r.status_code != 200: + msg = _("Failed to execute api %(api) : %(msg)s %(code)s") % { + 'api': api, + 'msg': r.json()['messages'][1]['text'], + 'code': r.status_code} + raise NimbleAPIException(msg) + return r.json() + + def _format_to_wwpn(self, string_wwpn): + return ':'.join(a + b for a, b in zip(* [iter(string_wwpn)] * 2)) - @_connection_checker - @_response_checker def get_fc_interface_list(self, array_name): - """getFibreChannelInterfaceList API to get FC interfaces on array""" - return self.client.service.getFibreChannelInterfaceList( - request={'sid': self.sid, - 'name-or-serial': array_name}) + """getFibreChannelInterfaceList API to get FC interfaces on array.""" + api = 'fibre_channel_interfaces/detail' + filter = {'array_name_or_serial': array_name} + r = self.get_query(api, filter) + if not r.json()['data']: + raise NimbleAPIException(_("No fc interfaces for array %s") % + array_name) + return r.json()['data'] + + def enable_group_scoped_target(self): + group_id = self.get_group_id() + api = "groups/" + six.text_type(group_id) + data = {'data': {'group_target_enabled': True}} + self.put(api, data) diff --git a/releasenotes/notes/nimble-rest-api-support-75c2324ee462d026.yaml b/releasenotes/notes/nimble-rest-api-support-75c2324ee462d026.yaml new file mode 100644 index 00000000000..f8843b6d4d9 --- /dev/null +++ b/releasenotes/notes/nimble-rest-api-support-75c2324ee462d026.yaml @@ -0,0 +1,4 @@ +--- +features: + - The Nimble backend driver has been updated to use REST + for array communication.