diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py index 5d8ce26087d..36370f9e8e9 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py @@ -618,17 +618,25 @@ AGGR_GET_ITER_SSC_RESPONSE = etree.XML(""" %(raid)s true + + %(node)s + %(aggr)s 1 -""" % {'aggr': VOLUME_AGGREGATE_NAME, 'raid': AGGREGATE_RAID_TYPE}) +""" % { + 'aggr': VOLUME_AGGREGATE_NAME, + 'raid': AGGREGATE_RAID_TYPE, + 'node': NODE_NAME, +}) AGGR_INFO_SSC = { 'name': VOLUME_AGGREGATE_NAME, 'raid-type': AGGREGATE_RAID_TYPE, 'is-hybrid': True, + 'node-name': NODE_NAME, } AGGR_SIZE_TOTAL = 107374182400 @@ -1309,14 +1317,3 @@ VSERVER_DATA_LIST_RESPONSE = etree.XML(""" 1 """ % {'vserver': VSERVER_NAME}) - -SYSTEM_NODE_GET_ITER_RESPONSE = etree.XML(""" - - - - %s - - - 1 - -""" % NODE_NAME) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py index 609b6ef9fda..76ba450ab96 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py @@ -53,6 +53,12 @@ class NetAppCmodeClientTestCase(test.TestCase): super(NetAppCmodeClientTestCase, self).setUp() self.mock_object(client_cmode.Client, '_init_ssh_client') + # store the original reference so we can call it later in + # test__get_cluster_nodes_info + self.original_get_cluster_nodes_info = ( + client_cmode.Client._get_cluster_nodes_info) + self.mock_object(client_cmode.Client, '_get_cluster_nodes_info', + return_value=fake.HYBRID_SYSTEM_NODES_INFO) self.mock_object(client_cmode.Client, 'get_ontap_version', return_value='9.6') with mock.patch.object(client_cmode.Client, @@ -216,6 +222,25 @@ class NetAppCmodeClientTestCase(test.TestCase): self.client.send_iter_request, 'storage-disk-get-iter') + @ddt.data((fake.AFF_SYSTEM_NODE_GET_ITER_RESPONSE, + fake.AFF_SYSTEM_NODES_INFO), + (fake.FAS_SYSTEM_NODE_GET_ITER_RESPONSE, + fake.FAS_SYSTEM_NODES_INFO), + (fake_client.NO_RECORDS_RESPONSE, []), + (fake.HYBRID_SYSTEM_NODE_GET_ITER_RESPONSE, + fake.HYBRID_SYSTEM_NODES_INFO)) + @ddt.unpack + def test__get_cluster_nodes_info(self, response, expected): + client_cmode.Client._get_cluster_nodes_info = ( + self.original_get_cluster_nodes_info) + nodes_response = netapp_api.NaElement(response) + self.mock_object(client_cmode.Client, 'send_iter_request', + return_value=nodes_response) + + result = self.client._get_cluster_nodes_info() + + self.assertEqual(expected, result) + def test_list_vservers(self): api_response = netapp_api.NaElement( @@ -829,44 +854,114 @@ class NetAppCmodeClientTestCase(test.TestCase): def test_provision_qos_policy_group_no_qos_policy_group_info(self): - self.client.provision_qos_policy_group(qos_policy_group_info=None) + self.client.provision_qos_policy_group(qos_policy_group_info=None, + qos_min_support=True) self.assertEqual(0, self.connection.qos_policy_group_create.call_count) def test_provision_qos_policy_group_legacy_qos_policy_group_info(self): self.client.provision_qos_policy_group( - qos_policy_group_info=fake.QOS_POLICY_GROUP_INFO_LEGACY) + qos_policy_group_info=fake.QOS_POLICY_GROUP_INFO_LEGACY, + qos_min_support=True) self.assertEqual(0, self.connection.qos_policy_group_create.call_count) + def test_provision_qos_policy_group_with_qos_spec_create_with_min(self): + + self.mock_object(self.client, + 'qos_policy_group_exists', + return_value=False) + mock_qos_policy_group_create = self.mock_object( + self.client, 'qos_policy_group_create') + mock_qos_policy_group_modify = self.mock_object( + self.client, 'qos_policy_group_modify') + + self.client.provision_qos_policy_group(fake.QOS_POLICY_GROUP_INFO, + True) + + mock_qos_policy_group_create.assert_has_calls([ + mock.call({ + 'policy_name': fake.QOS_POLICY_GROUP_NAME, + 'min_throughput': fake.MIN_IOPS, + 'max_throughput': fake.MAX_IOPS, + })]) + mock_qos_policy_group_modify.assert_not_called() + + def test_provision_qos_policy_group_with_qos_spec_create_unsupported(self): + mock_qos_policy_group_create = self.mock_object( + self.client, 'qos_policy_group_create') + mock_qos_policy_group_modify = self.mock_object( + self.client, 'qos_policy_group_modify') + + self.assertRaises( + netapp_utils.NetAppDriverException, + self.client.provision_qos_policy_group, + fake.QOS_POLICY_GROUP_INFO, False) + + mock_qos_policy_group_create.assert_not_called() + mock_qos_policy_group_modify.assert_not_called() + def test_provision_qos_policy_group_with_qos_spec_create(self): self.mock_object(self.client, 'qos_policy_group_exists', return_value=False) - self.mock_object(self.client, 'qos_policy_group_create') - self.mock_object(self.client, 'qos_policy_group_modify') + mock_qos_policy_group_create = self.mock_object( + self.client, 'qos_policy_group_create') + mock_qos_policy_group_modify = self.mock_object( + self.client, 'qos_policy_group_modify') - self.client.provision_qos_policy_group(fake.QOS_POLICY_GROUP_INFO) + self.client.provision_qos_policy_group(fake.QOS_POLICY_GROUP_INFO_MAX, + True) - self.client.qos_policy_group_create.assert_has_calls([ - mock.call(fake.QOS_POLICY_GROUP_NAME, fake.MAX_THROUGHPUT)]) - self.assertFalse(self.client.qos_policy_group_modify.called) + mock_qos_policy_group_create.assert_has_calls([ + mock.call({ + 'policy_name': fake.QOS_POLICY_GROUP_NAME, + 'max_throughput': fake.MAX_THROUGHPUT, + })]) + mock_qos_policy_group_modify.assert_not_called() + + def test_provision_qos_policy_group_with_qos_spec_modify_with_min(self): + + self.mock_object(self.client, + 'qos_policy_group_exists', + return_value=True) + mock_qos_policy_group_create = self.mock_object( + self.client, 'qos_policy_group_create') + mock_qos_policy_group_modify = self.mock_object( + self.client, 'qos_policy_group_modify') + + self.client.provision_qos_policy_group(fake.QOS_POLICY_GROUP_INFO, + True) + + mock_qos_policy_group_create.assert_not_called() + mock_qos_policy_group_modify.assert_has_calls([ + mock.call({ + 'policy_name': fake.QOS_POLICY_GROUP_NAME, + 'min_throughput': fake.MIN_IOPS, + 'max_throughput': fake.MAX_IOPS, + })]) def test_provision_qos_policy_group_with_qos_spec_modify(self): self.mock_object(self.client, 'qos_policy_group_exists', return_value=True) - self.mock_object(self.client, 'qos_policy_group_create') - self.mock_object(self.client, 'qos_policy_group_modify') + mock_qos_policy_group_create = self.mock_object( + self.client, 'qos_policy_group_create') + mock_qos_policy_group_modify = self.mock_object( + self.client, 'qos_policy_group_modify') - self.client.provision_qos_policy_group(fake.QOS_POLICY_GROUP_INFO) + self.client.provision_qos_policy_group(fake.QOS_POLICY_GROUP_INFO_MAX, + True) - self.assertFalse(self.client.qos_policy_group_create.called) - self.client.qos_policy_group_modify.assert_has_calls([ - mock.call(fake.QOS_POLICY_GROUP_NAME, fake.MAX_THROUGHPUT)]) + mock_qos_policy_group_create.assert_not_called() + mock_qos_policy_group_modify.assert_has_calls([ + mock.call({ + 'policy_name': fake.QOS_POLICY_GROUP_NAME, + 'max_throughput': fake.MAX_THROUGHPUT, + })]) def test_qos_policy_group_exists(self): @@ -906,12 +1001,16 @@ class NetAppCmodeClientTestCase(test.TestCase): api_args = { 'policy-group': fake.QOS_POLICY_GROUP_NAME, + 'min-throughput': '0', 'max-throughput': fake.MAX_THROUGHPUT, 'vserver': self.vserver, } - self.client.qos_policy_group_create( - fake.QOS_POLICY_GROUP_NAME, fake.MAX_THROUGHPUT) + self.client.qos_policy_group_create({ + 'policy_name': fake.QOS_POLICY_GROUP_NAME, + 'min_throughput': '0', + 'max_throughput': fake.MAX_THROUGHPUT, + }) self.mock_send_request.assert_has_calls([ mock.call('qos-policy-group-create', api_args, False)]) @@ -920,11 +1019,15 @@ class NetAppCmodeClientTestCase(test.TestCase): api_args = { 'policy-group': fake.QOS_POLICY_GROUP_NAME, + 'min-throughput': '0', 'max-throughput': fake.MAX_THROUGHPUT, } - self.client.qos_policy_group_modify( - fake.QOS_POLICY_GROUP_NAME, fake.MAX_THROUGHPUT) + self.client.qos_policy_group_modify({ + 'policy_name': fake.QOS_POLICY_GROUP_NAME, + 'min_throughput': '0', + 'max_throughput': fake.MAX_THROUGHPUT, + }) self.mock_send_request.assert_has_calls([ mock.call('qos-policy-group-modify', api_args, False)]) @@ -988,7 +1091,7 @@ class NetAppCmodeClientTestCase(test.TestCase): new_name = 'deleted_cinder_%s' % fake.QOS_POLICY_GROUP_NAME self.client.mark_qos_policy_group_for_deletion( - qos_policy_group_info=fake.QOS_POLICY_GROUP_INFO) + qos_policy_group_info=fake.QOS_POLICY_GROUP_INFO_MAX) mock_rename.assert_has_calls([ mock.call(fake.QOS_POLICY_GROUP_NAME, new_name)]) @@ -1005,7 +1108,7 @@ class NetAppCmodeClientTestCase(test.TestCase): new_name = 'deleted_cinder_%s' % fake.QOS_POLICY_GROUP_NAME self.client.mark_qos_policy_group_for_deletion( - qos_policy_group_info=fake.QOS_POLICY_GROUP_INFO) + qos_policy_group_info=fake.QOS_POLICY_GROUP_INFO_MAX) mock_rename.assert_has_calls([ mock.call(fake.QOS_POLICY_GROUP_NAME, new_name)]) @@ -2187,17 +2290,20 @@ class NetAppCmodeClientTestCase(test.TestCase): 'raid-type': None, 'is-hybrid': None, }, + 'aggr-ownership-attributes': { + 'home-name': None, + }, }, } self.client._get_aggregates.assert_has_calls([ mock.call( aggregate_names=[fake_client.VOLUME_AGGREGATE_NAME], desired_attributes=desired_attributes)]) - expected = { 'name': fake_client.VOLUME_AGGREGATE_NAME, 'raid-type': 'raid_dp', 'is-hybrid': True, + 'node-name': fake_client.NODE_NAME, } self.assertEqual(expected, result) @@ -2233,29 +2339,6 @@ class NetAppCmodeClientTestCase(test.TestCase): self.assertEqual({}, result) - def test_list_cluster_nodes(self): - - api_response = netapp_api.NaElement( - fake_client.SYSTEM_NODE_GET_ITER_RESPONSE) - self.mock_object(self.client.connection, - 'send_request', - mock.Mock(return_value=api_response)) - - result = self.client.list_cluster_nodes() - - self.assertListEqual([fake_client.NODE_NAME], result) - - def test_list_cluster_nodes_not_found(self): - - api_response = netapp_api.NaElement(fake_client.NO_RECORDS_RESPONSE) - self.mock_object(self.client.connection, - 'send_request', - mock.Mock(return_value=api_response)) - - result = self.client.list_cluster_nodes() - - self.assertListEqual([], result) - @ddt.data({'types': {'FCAL'}, 'expected': ['FCAL']}, {'types': {'SATA', 'SSD'}, 'expected': ['SATA', 'SSD']},) @ddt.unpack @@ -3591,3 +3674,23 @@ class NetAppCmodeClientTestCase(test.TestCase): self.client.connection.send_request.assert_called_once_with( 'snapshot-get-iter', api_args) self.assertListEqual(expected, result) + + @ddt.data(True, False) + def test_is_qos_min_supported(self, supported): + self.client.features.add_feature('test', supported=supported) + mock_name = self.mock_object(netapp_utils, + 'qos_min_feature_name', + return_value='test') + result = self.client.is_qos_min_supported(True, 'node') + + mock_name.assert_called_once_with(True, 'node') + self.assertEqual(result, supported) + + def test_is_qos_min_supported_invalid_node(self): + mock_name = self.mock_object(netapp_utils, + 'qos_min_feature_name', + return_value='invalid_feature') + result = self.client.is_qos_min_supported(True, 'node') + + mock_name.assert_called_once_with(True, 'node') + self.assertFalse(result) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py index 3f26ffc71eb..e5af4553178 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py @@ -260,6 +260,8 @@ IGROUP1 = {'initiator-group-os-type': 'linux', QOS_SPECS = {} EXTRA_SPECS = {} MAX_THROUGHPUT = '21734278B/s' +MIN_IOPS = '256IOPS' +MAX_IOPS = '512IOPS' QOS_POLICY_GROUP_NAME = 'fake_qos_policy_group_name' QOS_POLICY_GROUP_INFO_LEGACY = { @@ -268,11 +270,18 @@ QOS_POLICY_GROUP_INFO_LEGACY = { } QOS_POLICY_GROUP_SPEC = { + 'min_throughput': MIN_IOPS, + 'max_throughput': MAX_IOPS, + 'policy_name': QOS_POLICY_GROUP_NAME, +} + +QOS_POLICY_GROUP_SPEC_MAX = { 'max_throughput': MAX_THROUGHPUT, 'policy_name': QOS_POLICY_GROUP_NAME, } QOS_POLICY_GROUP_INFO = {'legacy': None, 'spec': QOS_POLICY_GROUP_SPEC} +QOS_POLICY_GROUP_INFO_MAX = {'legacy': None, 'spec': QOS_POLICY_GROUP_SPEC_MAX} CLONE_SOURCE_NAME = 'fake_clone_source_name' CLONE_SOURCE_ID = 'fake_clone_source_id' @@ -421,6 +430,103 @@ CG_VOLUME_SNAPSHOT = { 'volume_id': CG_VOLUME_ID, } +AFF_SYSTEM_NODE_GET_ITER_RESPONSE = etree.XML(""" + + + + AFFA400 + aff-node1 + true + false + + + AFFA400 + aff-node2 + true + false + + + 2 + +""") + +FAS_SYSTEM_NODE_GET_ITER_RESPONSE = etree.XML(""" + + + + FAS2554 + fas-node1 + false + false + + + FAS2554 + fas-node2 + false + false + + + 2 + +""") + +HYBRID_SYSTEM_NODE_GET_ITER_RESPONSE = etree.XML(""" + + + + select-node + false + true + FDvM300 + + + c190-node + true + false + AFF-C190 + + + 2 + +""") + +AFF_NODE = { + 'model': 'AFFA400', + 'is_all_flash': True, + 'is_all_flash_select': False, +} +AFF_NODE_1 = AFF_NODE.copy() +AFF_NODE_1['name'] = 'aff-node1' +AFF_NODE_2 = AFF_NODE.copy() +AFF_NODE_2['name'] = 'aff-node2' + +FAS_NODE = { + 'model': 'FAS2554', + 'is_all_flash': False, + 'is_all_flash_select': False, +} +FAS_NODE_1 = FAS_NODE.copy() +FAS_NODE_1['name'] = 'fas-node1' +FAS_NODE_2 = FAS_NODE.copy() +FAS_NODE_2['name'] = 'fas-node2' + +SELECT_NODE = { + 'model': 'FDvM300', + 'is_all_flash': False, + 'is_all_flash_select': True, + 'name': 'select-node', +} +C190_NODE = { + 'model': 'AFF-C190', + 'is_all_flash': True, + 'is_all_flash_select': False, + 'name': 'c190-node', +} + +AFF_SYSTEM_NODES_INFO = [AFF_NODE_1, AFF_NODE_2] +FAS_SYSTEM_NODES_INFO = [FAS_NODE_1, FAS_NODE_2] +HYBRID_SYSTEM_NODES_INFO = [SELECT_NODE, C190_NODE] + SYSTEM_GET_VERSION_RESPONSE = etree.XML(""" 1395426307 diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py index c7fb8d5e969..61c011f682e 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_block_cmode.py @@ -31,6 +31,7 @@ from cinder.volume.drivers.netapp.dataontap import block_base from cinder.volume.drivers.netapp.dataontap import block_cmode from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api from cinder.volume.drivers.netapp.dataontap.client import client_base +from cinder.volume.drivers.netapp.dataontap.client import client_cmode from cinder.volume.drivers.netapp.dataontap.performance import perf_cmode from cinder.volume.drivers.netapp.dataontap.utils import capabilities from cinder.volume.drivers.netapp.dataontap.utils import data_motion @@ -82,6 +83,11 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): config.netapp_api_trace_pattern = 'fake_regex' return config + @ddt.data(fake.AFF_SYSTEM_NODES_INFO, + fake.FAS_SYSTEM_NODES_INFO, + fake.HYBRID_SYSTEM_NODES_INFO) + @mock.patch.object(client_base.Client, 'get_ontap_version', + return_value='9.6') @mock.patch.object(perf_cmode, 'PerformanceCmodeLibrary', mock.Mock()) @mock.patch.object(client_base.Client, 'get_ontapi_version', mock.MagicMock(return_value=(1, 20))) @@ -91,11 +97,14 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): 'check_api_permissions') @mock.patch.object(na_utils, 'check_flags') @mock.patch.object(block_base.NetAppBlockStorageLibrary, 'do_setup') - @mock.patch.object(client_base.Client, 'get_ontap_version', - mock.MagicMock(return_value='9.6')) - def test_do_setup(self, super_do_setup, mock_check_flags, - mock_check_api_permissions, mock_cluster_user_supported): + def test_do_setup(self, cluster_nodes_info, + super_do_setup, mock_check_flags, + mock_check_api_permissions, mock_cluster_user_supported, + mock_get_ontap_version): self.mock_object(client_base.Client, '_init_ssh_client') + mock_get_cluster_nodes_info = self.mock_object( + client_cmode.Client, '_get_cluster_nodes_info', + return_value=cluster_nodes_info) self.mock_object( dot_utils, 'get_backend_configuration', return_value=self.get_config_cmode()) @@ -107,6 +116,8 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): self.assertEqual(1, mock_check_flags.call_count) mock_check_api_permissions.assert_called_once_with() mock_cluster_user_supported.assert_called_once_with() + mock_get_ontap_version.assert_called_once_with(cached=False) + mock_get_cluster_nodes_info.assert_called_once_with() def test_check_for_setup_error(self): super_check_for_setup_error = self.mock_object( @@ -559,13 +570,21 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): self.mock_object(na_utils, 'get_valid_qos_policy_group_info', return_value=fake.QOS_POLICY_GROUP_INFO) self.mock_object(self.zapi_client, 'provision_qos_policy_group') + mock_is_qos_min_supported = self.mock_object(self.library.ssc_library, + 'is_qos_min_supported', + return_value=True) + mock_extract_host = self.mock_object(volume_utils, 'extract_host', + return_value=fake.POOL_NAME) result = self.library._setup_qos_for_volume(fake.VOLUME, fake.EXTRA_SPECS) self.assertEqual(fake.QOS_POLICY_GROUP_INFO, result) self.zapi_client.provision_qos_policy_group.\ - assert_called_once_with(fake.QOS_POLICY_GROUP_INFO) + assert_called_once_with(fake.QOS_POLICY_GROUP_INFO, True) + mock_is_qos_min_supported.assert_called_once_with(fake.POOL_NAME) + mock_extract_host.assert_called_once_with(fake.VOLUME['host'], + level='pool') def test_setup_qos_for_volume_exception_path(self): self.mock_object(na_utils, 'get_valid_qos_policy_group_info', @@ -653,6 +672,9 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase): self.mock_object(na_utils, 'get_volume_extra_specs') self.mock_object(na_utils, 'log_extra_spec_warnings') self.library._check_volume_type_for_lun = mock.Mock() + self.library._setup_qos_for_volume = mock.Mock() + self.mock_object(na_utils, 'get_qos_policy_group_name_from_info', + return_value=None) self.library._add_lun_to_table = mock.Mock() self.zapi_client.move_lun = mock.Mock() diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py index 9bc2d3509eb..ae1982434ec 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_cmode.py @@ -627,15 +627,23 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): mock_debug_log = self.mock_object(nfs_cmode.LOG, 'debug') mock_cleanup = self.mock_object(self.driver, '_cleanup_volume_on_failure') + mock_is_qos_min_supported = self.mock_object(self.driver.ssc_library, + 'is_qos_min_supported', + return_value=True) + mock_extract_host = self.mock_object(volume_utils, 'extract_host', + return_value=fake.POOL_NAME) self.driver._do_qos_for_volume(fake.NFS_VOLUME, fake.EXTRA_SPECS) mock_get_info.assert_has_calls([ mock.call(fake.NFS_VOLUME, fake.EXTRA_SPECS)]) mock_provision_qos.assert_has_calls([ - mock.call(fake.QOS_POLICY_GROUP_INFO)]) + mock.call(fake.QOS_POLICY_GROUP_INFO, True)]) mock_set_policy.assert_has_calls([ mock.call(fake.NFS_VOLUME, fake.QOS_POLICY_GROUP_INFO, False)]) + mock_is_qos_min_supported.assert_called_once_with(fake.POOL_NAME) + mock_extract_host.assert_called_once_with(fake.NFS_VOLUME['host'], + level='pool') self.assertEqual(0, mock_error_log.call_count) self.assertEqual(0, mock_debug_log.call_count) self.assertEqual(0, mock_cleanup.call_count) @@ -652,6 +660,11 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): mock_debug_log = self.mock_object(nfs_cmode.LOG, 'debug') mock_cleanup = self.mock_object(self.driver, '_cleanup_volume_on_failure') + mock_is_qos_min_supported = self.mock_object(self.driver.ssc_library, + 'is_qos_min_supported', + return_value=True) + mock_extract_host = self.mock_object(volume_utils, 'extract_host', + return_value=fake.POOL_NAME) self.assertRaises(netapp_api.NaApiError, self.driver._do_qos_for_volume, @@ -661,9 +674,12 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): mock_get_info.assert_has_calls([ mock.call(fake.NFS_VOLUME, fake.EXTRA_SPECS)]) mock_provision_qos.assert_has_calls([ - mock.call(fake.QOS_POLICY_GROUP_INFO)]) + mock.call(fake.QOS_POLICY_GROUP_INFO, True)]) mock_set_policy.assert_has_calls([ mock.call(fake.NFS_VOLUME, fake.QOS_POLICY_GROUP_INFO, False)]) + mock_is_qos_min_supported.assert_called_once_with(fake.POOL_NAME) + mock_extract_host.assert_called_once_with(fake.NFS_VOLUME['host'], + level='pool') self.assertEqual(1, mock_error_log.call_count) self.assertEqual(1, mock_debug_log.call_count) mock_cleanup.assert_has_calls([ diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py index 9055261242d..c37a73e5dd5 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py @@ -42,6 +42,7 @@ SSC = { 'netapp_disk_type': ['SSD'], 'netapp_hybrid_aggregate': 'false', 'netapp_flexvol_encryption': 'true', + 'netapp_qos_min_support': 'true', 'pool_name': 'volume1', }, 'volume2': { @@ -56,6 +57,7 @@ SSC = { 'netapp_disk_type': ['FCAL', 'SSD'], 'netapp_hybrid_aggregate': 'true', 'netapp_flexvol_encryption': 'false', + 'netapp_qos_min_support': 'false', 'pool_name': 'volume2', }, } @@ -95,6 +97,15 @@ SSC_ENCRYPTION_INFO = { }, } +SSC_QOS_MIN_INFO = { + 'volume1': { + 'netapp_qos_min_support': 'true', + }, + 'volume2': { + 'netapp_qos_min_support': 'false', + }, +} + SSC_MIRROR_INFO = { 'volume1': { 'netapp_mirrored': 'false', @@ -109,11 +120,13 @@ SSC_AGGREGATE_INFO = { 'netapp_disk_type': ['SSD'], 'netapp_raid_type': 'raid_dp', 'netapp_hybrid_aggregate': 'false', + 'netapp_node_name': 'node1', }, 'volume2': { 'netapp_disk_type': ['FCAL', 'SSD'], 'netapp_raid_type': 'raid_dp', 'netapp_hybrid_aggregate': 'true', + 'netapp_node_name': 'node2', }, } diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_capabilities.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_capabilities.py index 65ea43b76c4..2447f74cb84 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_capabilities.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_capabilities.py @@ -117,6 +117,18 @@ class CapabilitiesLibraryTestCase(test.TestCase): six.assertCountEqual(self, list(fake.SSC_AGGREGATES), result) + def test_is_qos_min_supported(self): + ssc_pool = fake.SSC.get(fake.SSC_VOLUMES[0]) + is_qos_min = ssc_pool['netapp_qos_min_support'] == 'true' + result = self.ssc_library.is_qos_min_supported(ssc_pool['pool_name']) + + self.assertEqual(is_qos_min, result) + + def test_is_qos_min_supported_not_found(self): + result = self.ssc_library.is_qos_min_supported('invalid_pool') + + self.assertFalse(result) + def test_update_ssc(self): mock_get_ssc_flexvol_info = self.mock_object( @@ -139,6 +151,11 @@ class CapabilitiesLibraryTestCase(test.TestCase): self.ssc_library, '_get_ssc_encryption_info', side_effect=[fake.SSC_ENCRYPTION_INFO['volume1'], fake.SSC_ENCRYPTION_INFO['volume2']]) + mock_get_ssc_qos_min_info = self.mock_object( + self.ssc_library, '_get_ssc_qos_min_info', + side_effect=[fake.SSC_QOS_MIN_INFO['volume1'], + fake.SSC_QOS_MIN_INFO['volume2']]) + ordered_ssc = collections.OrderedDict() ordered_ssc['volume1'] = fake.SSC_VOLUME_MAP['volume1'] ordered_ssc['volume2'] = fake.SSC_VOLUME_MAP['volume2'] @@ -157,6 +174,8 @@ class CapabilitiesLibraryTestCase(test.TestCase): mock.call('aggr1'), mock.call('aggr2')]) mock_get_ssc_encryption_info.assert_has_calls([ mock.call('volume1'), mock.call('volume2')]) + mock_get_ssc_qos_min_info.assert_has_calls([ + mock.call('node1'), mock.call('node2')]) def test__update_for_failover(self): self.mock_object(self.ssc_library, 'update_ssc') @@ -346,6 +365,7 @@ class CapabilitiesLibraryTestCase(test.TestCase): 'netapp_disk_type': None, 'netapp_raid_type': None, 'netapp_hybrid_aggregate': None, + 'netapp_node_name': None, } self.zapi_client.get_aggregate.assert_not_called() self.zapi_client.get_aggregate_disk_types.assert_not_called() @@ -354,6 +374,7 @@ class CapabilitiesLibraryTestCase(test.TestCase): 'netapp_disk_type': fake_client.AGGREGATE_DISK_TYPES, 'netapp_raid_type': fake_client.AGGREGATE_RAID_TYPE, 'netapp_hybrid_aggregate': 'true', + 'netapp_node_name': fake_client.NODE_NAME, } self.zapi_client.get_aggregate.assert_called_once_with( fake_client.VOLUME_AGGREGATE_NAME) @@ -377,6 +398,7 @@ class CapabilitiesLibraryTestCase(test.TestCase): 'netapp_disk_type': None, 'netapp_raid_type': None, 'netapp_hybrid_aggregate': None, + 'netapp_node_name': None, } self.assertEqual(expected, result) @@ -506,3 +528,18 @@ class CapabilitiesLibraryTestCase(test.TestCase): self.assertFalse(self.ssc_library.cluster_user_supported()) else: self.assertTrue(self.ssc_library.cluster_user_supported()) + + def test_get_ssc_qos_min_info(self): + + self.mock_object( + self.ssc_library.zapi_client, 'is_qos_min_supported', + return_value=True) + + result = self.ssc_library._get_ssc_qos_min_info('node') + + expected = { + 'netapp_qos_min_support': 'true', + } + self.assertEqual(expected, result) + self.zapi_client.is_qos_min_supported.assert_called_once_with(False, + 'node') diff --git a/cinder/tests/unit/volume/drivers/netapp/fakes.py b/cinder/tests/unit/volume/drivers/netapp/fakes.py index 28536f12703..d9ddc403836 100644 --- a/cinder/tests/unit/volume/drivers/netapp/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/fakes.py @@ -94,7 +94,7 @@ QOS_SPECS = {} EXTRA_SPECS = {} -MAX_THROUGHPUT = '21734278B/s' +MAX_THROUGHPUT_BPS = '21734278B/s' QOS_POLICY_GROUP_NAME = 'fake_qos_policy_group_name' LEGACY_EXTRA_SPECS = {'netapp:qos_policy_group': QOS_POLICY_GROUP_NAME} @@ -103,7 +103,7 @@ LEGACY_QOS = { } QOS_POLICY_GROUP_SPEC = { - 'max_throughput': MAX_THROUGHPUT, + 'max_throughput': MAX_THROUGHPUT_BPS, 'policy_name': 'openstack-%s' % VOLUME_ID, } diff --git a/cinder/tests/unit/volume/drivers/netapp/test_utils.py b/cinder/tests/unit/volume/drivers/netapp/test_utils.py index 2081c42f04e..39ffbd4a27c 100644 --- a/cinder/tests/unit/volume/drivers/netapp/test_utils.py +++ b/cinder/tests/unit/volume/drivers/netapp/test_utils.py @@ -245,25 +245,46 @@ class NetAppDriverUtilsTestCase(test.TestCase): na_utils.validate_qos_spec(qos_spec) def test_validate_qos_spec_keys_weirdly_cased(self): - qos_spec = {'mAxIopS': 33000} + qos_spec = {'mAxIopS': 33000, 'mInIopS': 0} # Just return without raising an exception. na_utils.validate_qos_spec(qos_spec) - def test_validate_qos_spec_bad_key(self): + def test_validate_qos_spec_bad_key_max_flops(self): qos_spec = {'maxFlops': 33000} self.assertRaises(exception.Invalid, na_utils.validate_qos_spec, qos_spec) - def test_validate_qos_spec_bad_key_combination(self): + def test_validate_qos_spec_bad_key_min_bps(self): + qos_spec = {'minBps': 33000} + + self.assertRaises(exception.Invalid, + na_utils.validate_qos_spec, + qos_spec) + + def test_validate_qos_spec_bad_key_min_bps_per_gib(self): + qos_spec = {'minBPSperGiB': 33000} + + self.assertRaises(exception.Invalid, + na_utils.validate_qos_spec, + qos_spec) + + def test_validate_qos_spec_bad_key_combination_max_iops_max_bps(self): qos_spec = {'maxIOPS': 33000, 'maxBPS': 10000000} self.assertRaises(exception.Invalid, na_utils.validate_qos_spec, qos_spec) + def test_validate_qos_spec_bad_key_combination_miniops_miniopspergib(self): + qos_spec = {'minIOPS': 33000, 'minIOPSperGiB': 10000000} + + self.assertRaises(exception.Invalid, + na_utils.validate_qos_spec, + qos_spec) + def test_map_qos_spec_none(self): qos_spec = None @@ -271,6 +292,30 @@ class NetAppDriverUtilsTestCase(test.TestCase): self.assertIsNone(result) + def test_map_qos_spec_bad_key_combination_miniops_maxbpspergib(self): + qos_spec = {'minIOPS': 33000, 'maxBPSperGiB': 10000000} + + self.assertRaises(exception.Invalid, + na_utils.map_qos_spec, + qos_spec, + fake.VOLUME) + + def test_map_qos_spec_bad_key_combination_min_iops_max_bps(self): + qos_spec = {'minIOPS': 33000, 'maxBPS': 10000000} + + self.assertRaises(exception.Invalid, + na_utils.map_qos_spec, + qos_spec, + fake.VOLUME) + + def test_map_qos_spec_miniops_greater_than_maxiops(self): + qos_spec = {'minIOPS': 33001, 'maxIOPS': 33000} + + self.assertRaises(exception.Invalid, + na_utils.map_qos_spec, + qos_spec, + fake.VOLUME) + def test_map_qos_spec_maxiops(self): qos_spec = {'maxIOPs': 33000} mock_get_name = self.mock_object(na_utils, 'get_qos_policy_group_name') @@ -297,6 +342,20 @@ class NetAppDriverUtilsTestCase(test.TestCase): self.assertEqual(expected, result) + def test_map_qos_spec_miniopspergib_maxiopspergib(self): + qos_spec = {'minIOPSperGiB': 1000, 'maxIOPSperGiB': 1000} + mock_get_name = self.mock_object(na_utils, 'get_qos_policy_group_name') + mock_get_name.return_value = 'fake_qos_policy' + expected = { + 'policy_name': 'fake_qos_policy', + 'min_throughput': '42000iops', + 'max_throughput': '42000iops', + } + + result = na_utils.map_qos_spec(qos_spec, fake.VOLUME) + + self.assertEqual(expected, result) + def test_map_qos_spec_maxbps(self): qos_spec = {'maxBPS': 1000000} mock_get_name = self.mock_object(na_utils, 'get_qos_policy_group_name') @@ -329,7 +388,20 @@ class NetAppDriverUtilsTestCase(test.TestCase): mock_get_name.return_value = 'fake_qos_policy' expected = { 'policy_name': 'fake_qos_policy', - 'max_throughput': None, + } + + result = na_utils.map_qos_spec(qos_spec, fake.VOLUME) + + self.assertEqual(expected, result) + + def test_map_qos_spec_miniops_maxiops(self): + qos_spec = {'minIOPs': 25000, 'maxIOPs': 33000} + mock_get_name = self.mock_object(na_utils, 'get_qos_policy_group_name') + mock_get_name.return_value = 'fake_qos_policy' + expected = { + 'policy_name': 'fake_qos_policy', + 'min_throughput': '25000iops', + 'max_throughput': '33000iops', } result = na_utils.map_qos_spec(qos_spec, fake.VOLUME) @@ -586,6 +658,16 @@ class NetAppDriverUtilsTestCase(test.TestCase): na_utils.get_export_host_junction_path, share) + @ddt.data(True, False) + def test_qos_min_feature_name(self, is_nfs): + name = 'node' + feature_name = na_utils.qos_min_feature_name(is_nfs, name) + + if is_nfs: + self.assertEqual('QOS_MIN_NFS_' + name, feature_name) + else: + self.assertEqual('QOS_MIN_BLOCK_' + name, feature_name) + class OpenStackInfoTestCase(test.TestCase): diff --git a/cinder/volume/drivers/netapp/dataontap/block_cmode.py b/cinder/volume/drivers/netapp/dataontap/block_cmode.py index a4b3a5b586d..35e4d378065 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/block_cmode.py @@ -402,7 +402,10 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary, msg = _('Invalid QoS specification detected while getting QoS ' 'policy for volume %s') % volume['id'] raise exception.VolumeBackendAPIException(data=msg) - self.zapi_client.provision_qos_policy_group(qos_policy_group_info) + pool = volume_utils.extract_host(volume['host'], level='pool') + qos_min_support = self.ssc_library.is_qos_min_supported(pool) + self.zapi_client.provision_qos_policy_group(qos_policy_group_info, + qos_min_support) return qos_policy_group_info def _get_volume_model_update(self, volume): diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py index 0d4885f9ac6..9af979ad268 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py @@ -34,6 +34,8 @@ from cinder.volume import volume_utils LOG = logging.getLogger(__name__) DEFAULT_MAX_PAGE_LENGTH = 50 +ONTAP_SELECT_MODEL = 'FDvM300' +ONTAP_C190 = 'C190' @six.add_metaclass(volume_utils.TraceWrapperMetaclass) @@ -62,6 +64,26 @@ class Client(client_base.Client): ontapi_1_30 = ontapi_version >= (1, 30) ontapi_1_100 = ontapi_version >= (1, 100) ontapi_1_1xx = (1, 100) <= ontapi_version < (1, 200) + ontapi_1_60 = ontapi_version >= (1, 160) + + nodes_info = self._get_cluster_nodes_info() + for node in nodes_info: + qos_min_block = False + qos_min_nfs = False + if node['model'] == ONTAP_SELECT_MODEL: + qos_min_block = node['is_all_flash_select'] and ontapi_1_60 + qos_min_nfs = qos_min_block + elif ONTAP_C190 in node['model']: + qos_min_block = node['is_all_flash'] and ontapi_1_60 + qos_min_nfs = qos_min_block + else: + qos_min_block = node['is_all_flash'] and ontapi_1_20 + qos_min_nfs = node['is_all_flash'] and ontapi_1_30 + + qos_name = na_utils.qos_min_feature_name(True, node['name']) + self.features.add_feature(qos_name, supported=qos_min_nfs) + qos_name = na_utils.qos_min_feature_name(False, node['name']) + self.features.add_feature(qos_name, supported=qos_min_block) self.features.add_feature('SNAPMIRROR_V2', supported=ontapi_1_20) self.features.add_feature('USER_CAPABILITY_LIST', @@ -147,6 +169,45 @@ class Client(client_base.Client): result.get_child_by_name('next-tag').set_content('') return result + def _get_cluster_nodes_info(self): + """Return a list of models of the nodes in the cluster""" + api_args = { + 'desired-attributes': { + 'node-details-info': { + 'node': None, + 'node-model': None, + 'is-all-flash-select-optimized': None, + 'is-all-flash-optimized': None, + } + } + } + + nodes = [] + try: + result = self.send_iter_request('system-node-get-iter', api_args, + enable_tunneling=False) + system_node_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + for system_node in system_node_list.get_children(): + node = { + 'model': system_node.get_child_content('node-model'), + 'name': system_node.get_child_content('node'), + 'is_all_flash': system_node.get_child_content( + 'is-all-flash-optimized') == 'true', + 'is_all_flash_select': system_node.get_child_content( + 'is-all-flash-select-optimized') == 'true', + } + nodes.append(node) + + except netapp_api.NaApiError as e: + if e.code == netapp_api.EAPINOTFOUND: + LOG.debug('Cluster nodes can only be collected with ' + 'cluster scoped credentials.') + else: + LOG.exception('Failed to get the cluster nodes.') + + return nodes + def list_vservers(self, vserver_type='data'): """Get the names of vservers present, optionally filtered by type.""" query = { @@ -538,7 +599,8 @@ class Client(client_base.Client): } return self.connection.send_request('file-assign-qos', api_args, False) - def provision_qos_policy_group(self, qos_policy_group_info): + def provision_qos_policy_group(self, qos_policy_group_info, + qos_min_support): """Create QOS policy group on the backend if appropriate.""" if qos_policy_group_info is None: return @@ -546,17 +608,19 @@ class Client(client_base.Client): # Legacy QOS uses externally provisioned QOS policy group, # so we don't need to create one on the backend. legacy = qos_policy_group_info.get('legacy') - if legacy is not None: + if legacy: return spec = qos_policy_group_info.get('spec') - if spec is not None: + if spec: + if spec.get('min_throughput') and not qos_min_support: + msg = _('QoS min_throughput is not supported by this back ' + 'end.') + raise na_utils.NetAppDriverException(msg) if not self.qos_policy_group_exists(spec['policy_name']): - self.qos_policy_group_create(spec['policy_name'], - spec['max_throughput']) + self.qos_policy_group_create(spec) else: - self.qos_policy_group_modify(spec['policy_name'], - spec['max_throughput']) + self.qos_policy_group_modify(spec) def qos_policy_group_exists(self, qos_policy_group_name): """Checks if a QOS policy group exists.""" @@ -577,22 +641,24 @@ class Client(client_base.Client): False) return self._has_records(result) - def qos_policy_group_create(self, qos_policy_group_name, max_throughput): + def _qos_spec_to_api_args(self, spec, **kwargs): + """Convert a QoS spec to ZAPI args.""" + formatted_spec = {k.replace('_', '-'): v for k, v in spec.items() if v} + formatted_spec['policy-group'] = formatted_spec.pop('policy-name') + formatted_spec = {**formatted_spec, **kwargs} + + return formatted_spec + + def qos_policy_group_create(self, spec): """Creates a QOS policy group.""" - api_args = { - 'policy-group': qos_policy_group_name, - 'max-throughput': max_throughput, - 'vserver': self.vserver, - } + api_args = self._qos_spec_to_api_args( + spec, vserver=self.vserver) return self.connection.send_request( 'qos-policy-group-create', api_args, False) - def qos_policy_group_modify(self, qos_policy_group_name, max_throughput): + def qos_policy_group_modify(self, spec): """Modifies a QOS policy group.""" - api_args = { - 'policy-group': qos_policy_group_name, - 'max-throughput': max_throughput, - } + api_args = self._qos_spec_to_api_args(spec) return self.connection.send_request( 'qos-policy-group-modify', api_args, False) @@ -835,22 +901,6 @@ class Client(client_base.Client): return True - def list_cluster_nodes(self): - """Get all available cluster nodes.""" - - api_args = { - 'desired-attributes': { - 'node-details-info': { - 'node': None, - }, - }, - } - result = self.send_iter_request('system-node-get-iter', api_args) - nodes_info_list = result.get_child_by_name( - 'attributes-list') or netapp_api.NaElement('none') - return [node_info.get_child_content('node') for node_info - in nodes_info_list.get_children()] - def get_operational_lif_addresses(self): """Gets the IP addresses of operational LIFs on the vserver.""" @@ -1233,6 +1283,11 @@ class Client(client_base.Client): return True + def is_qos_min_supported(self, is_nfs, node_name): + """Check if the node supports QoS minimum.""" + qos_min_name = na_utils.qos_min_feature_name(is_nfs, node_name) + return getattr(self.features, qos_min_name, False).__bool__() + def create_flexvol(self, flexvol_name, aggregate_name, size_gb, space_guarantee_type=None, snapshot_policy=None, language=None, dedupe_enabled=False, @@ -1415,6 +1470,9 @@ class Client(client_base.Client): 'raid-type': None, 'is-hybrid': None, }, + 'aggr-ownership-attributes': { + 'home-name': None, + }, }, } @@ -1432,12 +1490,15 @@ class Client(client_base.Client): aggr_attributes = aggrs[0] aggr_raid_attrs = aggr_attributes.get_child_by_name( 'aggr-raid-attributes') or netapp_api.NaElement('none') + aggr_ownership_attrs = aggrs[0].get_child_by_name( + 'aggr-ownership-attributes') or netapp_api.NaElement('none') aggregate = { 'name': aggr_attributes.get_child_content('aggregate-name'), 'raid-type': aggr_raid_attrs.get_child_content('raid-type'), 'is-hybrid': strutils.bool_from_string( aggr_raid_attrs.get_child_content('is-hybrid')), + 'node-name': aggr_ownership_attrs.get_child_content('home-name'), } return aggregate diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py index a00cb01ad31..df95aa8523a 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py @@ -165,7 +165,10 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver, try: qos_policy_group_info = na_utils.get_valid_qos_policy_group_info( volume, extra_specs) - self.zapi_client.provision_qos_policy_group(qos_policy_group_info) + pool = volume_utils.extract_host(volume['host'], level='pool') + qos_min_support = self.ssc_library.is_qos_min_supported(pool) + self.zapi_client.provision_qos_policy_group(qos_policy_group_info, + qos_min_support) self._set_qos_policy_group_on_volume(volume, qos_policy_group_info, qos_policy_group_is_adaptive) except Exception: diff --git a/cinder/volume/drivers/netapp/dataontap/utils/capabilities.py b/cinder/volume/drivers/netapp/dataontap/utils/capabilities.py index 81a9b0985de..7e3222d4317 100644 --- a/cinder/volume/drivers/netapp/dataontap/utils/capabilities.py +++ b/cinder/volume/drivers/netapp/dataontap/utils/capabilities.py @@ -117,6 +117,15 @@ class CapabilitiesLibrary(object): aggregates.add(flexvol_info['netapp_aggregate']) return list(aggregates) + def is_qos_min_supported(self, pool_name): + for __, flexvol_info in self.ssc.items(): + if ('netapp_qos_min_support' in flexvol_info and + 'pool_name' in flexvol_info and + flexvol_info['pool_name'] == pool_name): + return flexvol_info['netapp_qos_min_support'] == 'true' + + return False + def update_ssc(self, flexvol_map): """Periodically runs to update Storage Service Catalog data. @@ -143,7 +152,11 @@ class CapabilitiesLibrary(object): # Get aggregate info aggregate_name = ssc_volume.get('netapp_aggregate') - ssc_volume.update(self._get_ssc_aggregate_info(aggregate_name)) + aggr_info = self._get_ssc_aggregate_info(aggregate_name) + node_name = aggr_info.pop('netapp_node_name') + ssc_volume.update(aggr_info) + + ssc_volume.update(self._get_ssc_qos_min_info(node_name)) ssc[flexvol_name] = ssc_volume @@ -212,6 +225,13 @@ class CapabilitiesLibrary(object): return {'netapp_flexvol_encryption': six.text_type(encrypted).lower()} + def _get_ssc_qos_min_info(self, node_name): + """Gather Qos minimum info and recast into SSC-style stats.""" + supported = self.zapi_client.is_qos_min_supported( + self.protocol == 'nfs', node_name) + + return {'netapp_qos_min_support': six.text_type(supported).lower()} + def _get_ssc_mirror_info(self, flexvol_name): """Gather SnapMirror info and recast into SSC-style volume stats.""" @@ -227,8 +247,10 @@ class CapabilitiesLibrary(object): raid_type = None hybrid = None disk_types = None + node_name = None else: aggregate = self.zapi_client.get_aggregate(aggregate_name) + node_name = aggregate.get('node-name') raid_type = aggregate.get('raid-type') hybrid = (six.text_type(aggregate.get('is-hybrid')).lower() if 'is-hybrid' in aggregate else None) @@ -239,6 +261,7 @@ class CapabilitiesLibrary(object): 'netapp_raid_type': raid_type, 'netapp_hybrid_aggregate': hybrid, 'netapp_disk_type': disk_types, + 'netapp_node_name': node_name, } def get_matching_flexvols_for_extra_specs(self, extra_specs): diff --git a/cinder/volume/drivers/netapp/utils.py b/cinder/volume/drivers/netapp/utils.py index a55c4c6e26c..a8bf11901b1 100644 --- a/cinder/volume/drivers/netapp/utils.py +++ b/cinder/volume/drivers/netapp/utils.py @@ -30,7 +30,6 @@ import re from oslo_concurrency import processutils as putils from oslo_log import log as logging from oslo_utils import netutils -import six from cinder import context from cinder import exception @@ -51,7 +50,16 @@ DEPRECATED_SSC_SPECS = {'netapp_unmirrored': 'netapp_mirrored', 'netapp_nodedup': 'netapp_dedup', 'netapp_nocompression': 'netapp_compression', 'netapp_thick_provisioned': 'netapp_thin_provisioned'} -QOS_KEYS = frozenset(['maxIOPS', 'maxIOPSperGiB', 'maxBPS', 'maxBPSperGiB']) +MIN_QOS_KEYS = frozenset([ + 'minIOPS', + 'minIOPSperGiB', +]) +MAX_QOS_KEYS = frozenset([ + 'maxIOPS', + 'maxIOPSperGiB', + 'maxBPS', + 'maxBPSperGiB', +]) BACKEND_QOS_CONSUMERS = frozenset(['back-end', 'both']) # Secret length cannot be less than 96 bits. http://tools.ietf.org/html/rfc3723 @@ -87,7 +95,7 @@ def check_flags(required_flags, configuration): def to_bool(val): """Converts true, yes, y, 1 to True, False otherwise.""" if val: - strg = six.text_type(val).lower() + strg = str(val).lower() if (strg == 'true' or strg == 'y' or strg == 'yes' or strg == 'enabled' or strg == '1'): @@ -152,7 +160,7 @@ def trace_filter_func_api(all_args): def round_down(value, precision='0.00'): - return float(decimal.Decimal(six.text_type(value)).quantize( + return float(decimal.Decimal(str(value)).quantize( decimal.Decimal(precision), rounding=decimal.ROUND_DOWN)) @@ -176,7 +184,7 @@ def get_iscsi_connection_properties(lun_id, volume, iqns, for a in addresses] lun_id = int(lun_id) - if isinstance(iqns, six.string_types): + if isinstance(iqns, str): iqns = [iqns] * len(addresses) target_portals = ['%s:%s' % (a, p) for a, p in zip(addresses, ports)] @@ -208,17 +216,28 @@ def validate_qos_spec(qos_spec): """Check validity of Cinder qos spec for our backend.""" if qos_spec is None: return - normalized_qos_keys = [key.lower() for key in QOS_KEYS] - keylist = [] - for key, value in qos_spec.items(): - lower_case_key = key.lower() - if lower_case_key not in normalized_qos_keys: - msg = _('Unrecognized QOS keyword: "%s"') % key - raise exception.Invalid(msg) - keylist.append(lower_case_key) - # Modify the following check when we allow multiple settings in one spec. - if len(keylist) > 1: - msg = _('Only one limit can be set in a QoS spec.') + + normalized_min_keys = [key.lower() for key in MIN_QOS_KEYS] + normalized_max_keys = [key.lower() for key in MAX_QOS_KEYS] + + unrecognized_keys = [ + k for k in qos_spec.keys() + if k.lower() not in normalized_max_keys + normalized_min_keys] + + if unrecognized_keys: + msg = _('Unrecognized QOS keywords: "%s"') % unrecognized_keys + raise exception.Invalid(msg) + + min_dict = {k: v for k, v in qos_spec.items() + if k.lower() in normalized_min_keys} + if len(min_dict) > 1: + msg = _('Only one minimum limit can be set in a QoS spec.') + raise exception.Invalid(msg) + + max_dict = {k: v for k, v in qos_spec.items() + if k.lower() in normalized_max_keys} + if len(max_dict) > 1: + msg = _('Only one maximum limit can be set in a QoS spec.') raise exception.Invalid(msg) @@ -231,28 +250,67 @@ def get_volume_type_from_volume(volume): return volume_types.get_volume_type(ctxt, type_id) +def _get_min_throughput_from_qos_spec(qos_spec, volume_size): + """Returns the minimum QoS throughput. + + The QoS min specs are exclusive of one another and it accepts values in + IOPS only. + """ + if 'miniops' in qos_spec: + min_throughput = '%siops' % qos_spec['miniops'] + elif 'miniopspergib' in qos_spec: + min_throughput = '%siops' % str( + int(qos_spec['miniopspergib']) * int(volume_size)) + else: + min_throughput = None + return min_throughput + + +def _get_max_throughput_from_qos_spec(qos_spec, volume_size): + """Returns the maximum QoS throughput. + + The QoS max specs are exclusive of one another. + """ + if 'maxiops' in qos_spec: + max_throughput = '%siops' % qos_spec['maxiops'] + elif 'maxiopspergib' in qos_spec: + max_throughput = '%siops' % str( + int(qos_spec['maxiopspergib']) * int(volume_size)) + elif 'maxbps' in qos_spec: + max_throughput = '%sB/s' % qos_spec['maxbps'] + elif 'maxbpspergib' in qos_spec: + max_throughput = '%sB/s' % str( + int(qos_spec['maxbpspergib']) * int(volume_size)) + else: + max_throughput = None + return max_throughput + + def map_qos_spec(qos_spec, volume): """Map Cinder QOS spec to limit/throughput-value as used in client API.""" if qos_spec is None: return None - qos_spec = map_dict_to_lower(qos_spec) - spec = dict(policy_name=get_qos_policy_group_name(volume), - max_throughput=None) + spec = map_dict_to_lower(qos_spec) + min_throughput = _get_min_throughput_from_qos_spec(spec, volume['size']) + max_throughput = _get_max_throughput_from_qos_spec(spec, volume['size']) - # QoS specs are exclusive of one another. - if 'maxiops' in qos_spec: - spec['max_throughput'] = '%siops' % qos_spec['maxiops'] - elif 'maxiopspergib' in qos_spec: - spec['max_throughput'] = '%siops' % six.text_type( - int(qos_spec['maxiopspergib']) * int(volume['size'])) - elif 'maxbps' in qos_spec: - spec['max_throughput'] = '%sB/s' % qos_spec['maxbps'] - elif 'maxbpspergib' in qos_spec: - spec['max_throughput'] = '%sB/s' % six.text_type( - int(qos_spec['maxbpspergib']) * int(volume['size'])) + if min_throughput and max_throughput and max_throughput.endswith('B/s'): + msg = _('Maximum limit should be in IOPS when minimum limit is ' + 'specified.') + raise exception.Invalid(msg) - return spec + if min_throughput and max_throughput and max_throughput < min_throughput: + msg = _('Maximum limit should be greater than or equal to the ' + 'minimum limit.') + raise exception.Invalid(msg) + + policy = dict(policy_name=get_qos_policy_group_name(volume)) + if min_throughput: + policy['min_throughput'] = min_throughput + if max_throughput: + policy['max_throughput'] = max_throughput + return policy def map_dict_to_lower(input_dict): @@ -394,6 +452,13 @@ def get_export_host_junction_path(share): return host, junction_path +def qos_min_feature_name(is_nfs, node_name): + if is_nfs: + return 'QOS_MIN_NFS_' + node_name + else: + return 'QOS_MIN_BLOCK_' + node_name + + class hashabledict(dict): """A hashable dictionary that is comparable (i.e. in unit tests, etc.)""" def __hash__(self): diff --git a/releasenotes/notes/bp-netapp-ontap-min-throughput-qos-cd3812df5c7da8fd.yaml b/releasenotes/notes/bp-netapp-ontap-min-throughput-qos-cd3812df5c7da8fd.yaml new file mode 100644 index 00000000000..78e84a2fae2 --- /dev/null +++ b/releasenotes/notes/bp-netapp-ontap-min-throughput-qos-cd3812df5c7da8fd.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + NetApp ONTAP driver: Added support for QoS Min (floor) throughput specs. + The driver now accepts ``minIOPS`` and ``minIOPSperGiB`` specs, which can + be set either individually or along with Max (ceiling) throughput specs. + The feature requires storage ONTAP All Flash FAS (AFF) with version equal + or greater than 9.3 for NFS and 9.2 for iSCSI and FCP. It also works with + Select Premium with SSD and C190 storages with at least ONTAP 9.6. + - | + NetApp ONTAP driver: Added a new driver specific capability called + `netapp_qos_min_support`. It is used to filter the pools that has support + to the Qos minimum (floor) specs during the scheduler phase.