From 57b972959468b8963df56425938753c9b8efe8c6 Mon Sep 17 00:00:00 2001 From: Tom Barron Date: Mon, 17 Aug 2015 14:08:28 -0400 Subject: [PATCH] Scheduler-based over-subscription for NFS drivers Add support to the NFS drivers for over-subscription/overprovisioning. This allows the Cinder scheduler to more accurately represent the remaining space on a pool when thin-provisioned Cinder Volumes are in use on an NFS backend. DocImpact Co-Authored-By: Clinton Knight Co-Authored-By: Mike Rooney Implements: blueprint nfs-over-subscription-thin-provisioning Change-Id: Ib94d5131554466709e168c18d1b8690dddd55ff0 --- cinder/tests/unit/test_hitachi_hnas_nfs.py | 4 + cinder/tests/unit/test_netapp_nfs.py | 4 + cinder/tests/unit/test_nfs.py | 200 ++++++++++-------- cinder/tests/unit/test_tintri.py | 1 + cinder/tests/unit/test_zfssa.py | 2 + .../volume/drivers/netapp/dataontap/fakes.py | 1 + .../netapp/dataontap/test_nfs_7mode.py | 45 ++-- .../drivers/netapp/dataontap/test_nfs_base.py | 13 +- .../netapp/dataontap/test_nfs_cmode.py | 96 +++++++-- .../drivers/netapp/dataontap/nfs_7mode.py | 4 + .../drivers/netapp/dataontap/nfs_base.py | 30 +-- .../drivers/netapp/dataontap/nfs_cmode.py | 12 +- cinder/volume/drivers/nfs.py | 119 +++++++++-- 13 files changed, 359 insertions(+), 172 deletions(-) diff --git a/cinder/tests/unit/test_hitachi_hnas_nfs.py b/cinder/tests/unit/test_hitachi_hnas_nfs.py index db1c3e75a..0277477cb 100644 --- a/cinder/tests/unit/test_hitachi_hnas_nfs.py +++ b/cinder/tests/unit/test_hitachi_hnas_nfs.py @@ -182,6 +182,8 @@ class HDSNFSDriverTest(test.TestCase): self.shares_file.flush() self.configuration = mock.Mock(spec=conf.Configuration) + self.configuration.max_over_subscription_ratio = 20.0 + self.configuration.reserved_percentage = 0 self.configuration.hds_hnas_nfs_config_file = self.config_file.name self.configuration.nfs_shares_config = self.shares_file.name self.configuration.nfs_mount_point_base = '/opt/stack/cinder/mnt' @@ -189,6 +191,8 @@ class HDSNFSDriverTest(test.TestCase): self.configuration.nas_ip = None self.configuration.nas_share_path = None self.configuration.nas_mount_options = None + self.configuration.nfs_used_ratio = .95 + self.configuration.nfs_oversub_ratio = 1.0 self.driver = nfs.HDSNFSDriver(configuration=self.configuration) self.driver.do_setup("") diff --git a/cinder/tests/unit/test_netapp_nfs.py b/cinder/tests/unit/test_netapp_nfs.py index d08d6ae89..77861ab99 100644 --- a/cinder/tests/unit/test_netapp_nfs.py +++ b/cinder/tests/unit/test_netapp_nfs.py @@ -85,9 +85,13 @@ FAKE_VSERVER = 'fake_vserver' def create_configuration(): configuration = mox_lib.MockObject(conf.Configuration) configuration.append_config_values(mox_lib.IgnoreArg()) + configuration.max_over_subscription_ratio = 20.0 + configuration.reserved_percentage = 0 configuration.nfs_mount_point_base = '/mnt/test' configuration.nfs_mount_options = None configuration.nas_mount_options = None + configuration.nfs_used_ratio = .95 + configuration.nfs_oversub_ratio = 1.0 configuration.netapp_server_hostname = CONNECTION_INFO['hostname'] configuration.netapp_transport_type = CONNECTION_INFO['transport_type'] configuration.netapp_server_port = CONNECTION_INFO['port'] diff --git a/cinder/tests/unit/test_nfs.py b/cinder/tests/unit/test_nfs.py index 53b932711..4432c7b1b 100644 --- a/cinder/tests/unit/test_nfs.py +++ b/cinder/tests/unit/test_nfs.py @@ -14,6 +14,7 @@ # under the License. """Unit tests for the NFS driver module.""" +import ddt import errno import os @@ -53,6 +54,8 @@ class RemoteFsDriverTestCase(test.TestCase): self.configuration.append_config_values(mox_lib.IgnoreArg()) self.configuration.nas_secure_file_permissions = 'false' self.configuration.nas_secure_file_operations = 'false' + self.configuration.max_over_subscription_ratio = 1.0 + self.configuration.reserved_percentage = 5 self._driver = remotefs.RemoteFSDriver( configuration=self.configuration) @@ -334,6 +337,7 @@ class RemoteFsDriverTestCase(test.TestCase): self.assertFalse(ret_flag) +@ddt.ddt class NfsDriverTestCase(test.TestCase): """Test case for NFS driver.""" @@ -358,10 +362,13 @@ class NfsDriverTestCase(test.TestCase): self.stubs = stubout.StubOutForTesting() self.configuration = mox_lib.MockObject(conf.Configuration) self.configuration.append_config_values(mox_lib.IgnoreArg()) + self.configuration.max_over_subscription_ratio = 1.0 + self.configuration.reserved_percentage = 5 self.configuration.nfs_shares_config = None self.configuration.nfs_sparsed_volumes = True self.configuration.nfs_used_ratio = 0.95 self.configuration.nfs_oversub_ratio = 1.0 + self.configuration.nfs_reserved_percentage = 5.0 self.configuration.nfs_mount_point_base = self.TEST_MNT_POINT_BASE self.configuration.nfs_mount_options = None self.configuration.nfs_mount_attempts = 3 @@ -371,7 +378,6 @@ class NfsDriverTestCase(test.TestCase): self.configuration.nas_ip = None self.configuration.nas_share_path = None self.configuration.nas_mount_options = None - self.configuration.reserved_percentage = 0 self.configuration.volume_dd_blocksize = '1M' self._driver = nfs.NfsDriver(configuration=self.configuration) self._driver.shares = {} @@ -620,7 +626,7 @@ class NfsDriverTestCase(test.TestCase): self.assertEqual(1, remotefs.LOG.error.call_count) - def test_find_share_should_throw_error_if_there_is_no_mounted_shares(self): + def test_find_share_should_throw_error_if_there_is_no_mounted_share(self): """_find_share should throw error if there is no mounted shares.""" drv = self._driver @@ -657,7 +663,7 @@ class NfsDriverTestCase(test.TestCase): mox.VerifyAll() - def test_find_share_should_throw_error_if_there_is_no_enough_place(self): + def test_find_share_should_throw_error_if_there_is_not_enough_space(self): """_find_share should throw error if there is no share to host vol.""" mox = self.mox drv = self._driver @@ -849,7 +855,7 @@ class NfsDriverTestCase(test.TestCase): drv.get_volume_stats() self.assertEqual(30.0, drv._stats['total_capacity_gb']) self.assertEqual(5.0, drv._stats['free_capacity_gb']) - self.assertEqual(0, drv._stats['reserved_percentage']) + self.assertEqual(5, drv._stats['reserved_percentage']) self.assertTrue(drv._stats['sparse_copy_volume']) mox.VerifyAll() @@ -857,8 +863,8 @@ class NfsDriverTestCase(test.TestCase): def test_get_volume_stats_with_non_zero_reserved_percentage(self): """get_volume_stats must fill the correct values.""" mox = self.mox - drv = self._driver self.configuration.reserved_percentage = 10.0 + drv = nfs.NfsDriver(configuration=self.configuration) drv._mounted_shares = [self.TEST_NFS_EXPORT1, self.TEST_NFS_EXPORT2] @@ -883,6 +889,83 @@ class NfsDriverTestCase(test.TestCase): self.assertEqual(10.0, drv._stats['reserved_percentage']) mox.VerifyAll() + @ddt.data(True, False) + def test_update_volume_stats(self, thin): + + self._driver.over_subscription_ratio = 20.0 + self._driver.reserved_percentage = 5.0 + self._driver.configuration.nfs_sparsed_volumes = thin + + remotefs_volume_stats = { + 'volume_backend_name': 'fake_backend_name', + 'vendor_name': 'fake_vendor', + 'driver_version': 'fake_version', + 'storage_protocol': 'NFS', + 'total_capacity_gb': 100.0, + 'free_capacity_gb': 20.0, + 'reserved_percentage': 5.0, + 'QoS_support': False, + } + self.mock_object(remotefs.RemoteFSDriver, '_update_volume_stats') + self._driver._stats = remotefs_volume_stats + + mock_get_provisioned_capacity = self.mock_object( + self._driver, '_get_provisioned_capacity', + mock.Mock(return_value=25.0)) + + self._driver._update_volume_stats() + + nfs_added_volume_stats = { + 'provisioned_capacity_gb': 25.0 if thin else 80.0, + 'max_over_subscription_ratio': 20.0, + 'reserved_percentage': 5.0, + 'thin_provisioning_support': thin, + 'thick_provisioning_support': not thin, + } + expected = remotefs_volume_stats + expected.update(nfs_added_volume_stats) + + self.assertEqual(expected, self._driver._stats) + self.assertEqual(thin, mock_get_provisioned_capacity.called) + + @ddt.data({'nfs_oversub_ratio': 1.0}, + {'nfs_oversub_ratio': 1.5}) + @ddt.unpack + def test_get_over_subscription_ratio(self, nfs_oversub_ratio): + self.configuration.nfs_oversub_ratio = nfs_oversub_ratio + self._driver.configuration = self.configuration + self.mock_object(nfs, 'LOG') + + oversub_ratio = self._driver._get_over_subscription_ratio() + + if nfs_oversub_ratio == 1.0: + self.assertEqual(1.0, oversub_ratio) + self.assertFalse(nfs.LOG.warn.called) + else: + self.assertEqual(nfs_oversub_ratio, oversub_ratio) + self.assertTrue(nfs.LOG.warn.called) + + @ddt.data({'nfs_used_ratio': 0.95, 'nfs_reserved_percentage': 0.05}, + {'nfs_used_ratio': 0.80, 'nfs_reserved_percentage': 0.20}) + @ddt.unpack + def test_get_reserved_percentage(self, nfs_used_ratio, + nfs_reserved_percentage): + self.configuration.nfs_used_ratio = nfs_used_ratio + self.configuration.nfs_reserved_percentage = nfs_reserved_percentage + self._driver.configuration = self.configuration + self.mock_object(nfs, 'LOG') + + reserved_percentage = self._driver._get_reserved_percentage() + + if nfs_used_ratio == 0.95: + self.assertEqual(self._driver.configuration.reserved_percentage, + reserved_percentage) + self.assertFalse(nfs.LOG.warn.called) + else: + expected = (1 - self._driver.configuration.nfs_used_ratio) * 100 + self.assertEqual(expected, reserved_percentage) + self.assertTrue(nfs.LOG.warn.called) + def _check_is_share_eligible(self, total_size, total_available, total_allocated, requested_volume_size): with mock.patch.object(self._driver, '_get_capacity_info')\ @@ -1132,118 +1215,61 @@ class NfsDriverDoSetupTestCase(test.TestCase): config.append_config_values(nfs.nfs_opts) self.configuration = config - def test_setup_should_throw_error_if_shares_config_not_configured(self): - """do_setup should throw error if shares config is not configured.""" - - self.override_config('nfs_shares_config', None) - drv = nfs.NfsDriver(configuration=self.configuration) - - mock_os_path_exists = self.mock_object(os.path, 'exists') - - with self.assertRaisesRegex(exception.NfsException, - ".*no NFS config file configured.*"): - drv.do_setup(self.context) - - self.assertEqual(0, mock_os_path_exists.call_count) - - def test_setup_should_throw_error_if_shares_file_does_not_exist(self): - """do_setup should throw error if shares file does not exist.""" - - drv = nfs.NfsDriver(configuration=self.configuration) - - mock_os_path_exists = self.mock_object(os.path, 'exists') - mock_os_path_exists.return_value = False - - with self.assertRaisesRegex(exception.NfsException, - "NFS config file.*doesn't exist"): - drv.do_setup(self.context) - - mock_os_path_exists.assert_has_calls( - [mock.call(self.configuration.nfs_shares_config)]) - - def test_setup_should_throw_error_if_oversub_ratio_less_than_zero(self): - """do_setup should throw error if nfs_oversub_ratio is less than 0.""" + def test_init_should_throw_error_if_oversub_ratio_less_than_zero(self): + """__init__ should throw error if nfs_oversub_ratio is less than 0.""" self.override_config('nfs_oversub_ratio', -1) - drv = nfs.NfsDriver(configuration=self.configuration) - - mock_os_path_exists = self.mock_object(os.path, 'exists') - mock_os_path_exists.return_value = True with self.assertRaisesRegex(exception.InvalidConfigurationValue, ".*'nfs_oversub_ratio' invalid.*"): - drv.do_setup(self.context) + nfs.NfsDriver(configuration=self.configuration) - mock_os_path_exists.assert_has_calls( - [mock.call(self.configuration.nfs_shares_config)]) + def test_init_should_throw_error_if_used_ratio_less_than_zero(self): + """__init__ should throw error if nfs_used_ratio is less than 0.""" - def test_setup_oversub_ratio_default_value(self): - """do_setup should work with default value for nfs_oversub_ratio.""" - - drv = nfs.NfsDriver(configuration=self.configuration) - - mock_os_path_exists = self.mock_object(os.path, 'exists') - mock_os_path_exists.return_value = True - mock_execute = self.mock_object(drv, '_execute') - mock_set_security = self.mock_object(drv, 'set_nas_security_options') - - drv.do_setup(self.context) - - mock_os_path_exists.assert_has_calls( - [mock.call(self.configuration.nfs_shares_config)]) - self.assertEqual(1, mock_execute.call_count) - self.assertEqual(1, mock_set_security.call_count) - self.assertEqual(1.0, self.configuration.nfs_oversub_ratio) + self.override_config('nfs_used_ratio', -1) - def test_setup_should_throw_error_if_used_ratio_less_than_zero(self): - """do_setup should throw error if nfs_used_ratio is less than 0.""" + with self.assertRaisesRegex(exception.InvalidConfigurationValue, + ".*'nfs_used_ratio' invalid.*"): + nfs.NfsDriver(configuration=self.configuration) - self.override_config('nfs_used_ratio', -1) - drv = nfs.NfsDriver(configuration=self.configuration) + def test_init_should_throw_error_if_used_ratio_greater_than_one(self): + """__init__ should throw error if nfs_used_ratio is greater than 1.""" - mock_os_path_exists = self.mock_object(os.path, 'exists') - mock_os_path_exists.return_value = True + self.override_config('nfs_used_ratio', 2) with self.assertRaisesRegex(exception.InvalidConfigurationValue, ".*'nfs_used_ratio' invalid.*"): - drv.do_setup(self.context) - - mock_os_path_exists.assert_has_calls( - [mock.call(self.configuration.nfs_shares_config)]) + nfs.NfsDriver(configuration=self.configuration) - def test_setup_should_throw_error_if_used_ratio_greater_than_one(self): - """do_setup should throw error if nfs_used_ratio is greater than 1.""" + def test_setup_should_throw_error_if_shares_config_not_configured(self): + """do_setup should throw error if shares config is not configured.""" - self.override_config('nfs_used_ratio', 2) + self.override_config('nfs_shares_config', None) drv = nfs.NfsDriver(configuration=self.configuration) mock_os_path_exists = self.mock_object(os.path, 'exists') - mock_os_path_exists.return_value = True - with self.assertRaisesRegex(exception.InvalidConfigurationValue, - ".*'nfs_used_ratio' invalid.*"): + with self.assertRaisesRegex(exception.NfsException, + ".*no NFS config file configured.*"): drv.do_setup(self.context) - mock_os_path_exists.assert_has_calls( - [mock.call(self.configuration.nfs_shares_config)]) + self.assertEqual(0, mock_os_path_exists.call_count) - def test_setup_used_ratio_default_value(self): - """do_setup should work with default value for nfs_used_ratio.""" + def test_setup_should_throw_error_if_shares_file_does_not_exist(self): + """do_setup should throw error if shares file does not exist.""" drv = nfs.NfsDriver(configuration=self.configuration) mock_os_path_exists = self.mock_object(os.path, 'exists') - mock_os_path_exists.return_value = True - mock_execute = self.mock_object(drv, '_execute') - mock_set_security = self.mock_object(drv, 'set_nas_security_options') + mock_os_path_exists.return_value = False - drv.do_setup(self.context) + with self.assertRaisesRegex(exception.NfsException, + "NFS config file.*doesn't exist"): + drv.do_setup(self.context) mock_os_path_exists.assert_has_calls( [mock.call(self.configuration.nfs_shares_config)]) - self.assertEqual(1, mock_execute.call_count) - self.assertEqual(1, mock_set_security.call_count) - self.assertEqual(0.95, self.configuration.nfs_used_ratio) def test_setup_should_throw_exception_if_nfs_client_is_not_installed(self): """do_setup should throw error if nfs client is not installed.""" diff --git a/cinder/tests/unit/test_tintri.py b/cinder/tests/unit/test_tintri.py index 04449a9de..d7cf98765 100644 --- a/cinder/tests/unit/test_tintri.py +++ b/cinder/tests/unit/test_tintri.py @@ -56,6 +56,7 @@ class TintriDriverTestCase(test.TestCase): configuration.nfs_mount_point_base = '/mnt/test' configuration.nfs_mount_options = None configuration.nas_mount_options = None + configuration.nfs_used_ratio = 0.95 return configuration def fake_stubs(self): diff --git a/cinder/tests/unit/test_zfssa.py b/cinder/tests/unit/test_zfssa.py index 719bb8025..3ce0a0d1e 100644 --- a/cinder/tests/unit/test_zfssa.py +++ b/cinder/tests/unit/test_zfssa.py @@ -737,6 +737,8 @@ class TestZFSSANFSDriver(test.TestCase): def _create_fake_config(self): self.configuration = mock.Mock(spec=conf.Configuration) + self.configuration.reserved_percentage = 0 + self.configuration.max_over_subscription_ratio = 20.0 self.configuration.san_ip = '1.1.1.1' self.configuration.san_login = 'user' self.configuration.san_password = 'passwd' diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py index 6192ae7c7..472f58e73 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/fakes.py @@ -158,6 +158,7 @@ NFS_SHARE_IPV4 = IPV4_ADDRESS + ':' + EXPORT_PATH NFS_SHARE_IPV6 = IPV6_ADDRESS + ':' + EXPORT_PATH RESERVED_PERCENTAGE = 7 +MAX_OVER_SUBSCRIPTION_RATIO = 19.0 TOTAL_BYTES = 4797892092432 AVAILABLE_BYTES = 13479932478 CAPACITY_VALUES = (TOTAL_BYTES, AVAILABLE_BYTES) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_7mode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_7mode.py index d15a09061..894ef2158 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_7mode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_7mode.py @@ -15,6 +15,7 @@ Unit tests for the NetApp 7mode NFS storage driver """ +import ddt import mock from os_brick.remotefs import remotefs as remotefs_brick from oslo_utils import units @@ -27,6 +28,7 @@ from cinder.volume.drivers.netapp.dataontap import nfs_7mode from cinder.volume.drivers.netapp import utils as na_utils +@ddt.ddt class NetApp7modeNfsDriverTestCase(test.TestCase): def setUp(self): super(NetApp7modeNfsDriverTestCase, self).setUp() @@ -52,28 +54,43 @@ class NetApp7modeNfsDriverTestCase(test.TestCase): config.netapp_server_port = '80' return config - def test_get_pool_stats(self): + @ddt.data({'nfs_sparsed_volumes': True}, + {'nfs_sparsed_volumes': False}) + @ddt.unpack + def test_get_pool_stats(self, nfs_sparsed_volumes): + + self.driver.configuration.nfs_sparsed_volumes = nfs_sparsed_volumes + thick = not nfs_sparsed_volumes total_capacity_gb = na_utils.round_down( fake.TOTAL_BYTES / units.Gi, '0.01') free_capacity_gb = na_utils.round_down( fake.AVAILABLE_BYTES / units.Gi, '0.01') - capacity = dict( - reserved_percentage = fake.RESERVED_PERCENTAGE, - total_capacity_gb = total_capacity_gb, - free_capacity_gb = free_capacity_gb, - ) - - mock_get_capacity = self.mock_object( - self.driver, '_get_share_capacity_info') - mock_get_capacity.return_value = capacity + provisioned_capacity_gb = total_capacity_gb - free_capacity_gb + capacity = { + 'reserved_percentage': fake.RESERVED_PERCENTAGE, + 'max_over_subscription_ratio': fake.MAX_OVER_SUBSCRIPTION_RATIO, + 'total_capacity_gb': total_capacity_gb, + 'free_capacity_gb': free_capacity_gb, + 'provisioned_capacity_gb': provisioned_capacity_gb, + } + self.mock_object(self.driver, + '_get_share_capacity_info', + mock.Mock(return_value=capacity)) result = self.driver._get_pool_stats() - self.assertEqual(fake.RESERVED_PERCENTAGE, - result[0]['reserved_percentage']) - self.assertEqual(total_capacity_gb, result[0]['total_capacity_gb']) - self.assertEqual(free_capacity_gb, result[0]['free_capacity_gb']) + expected = [{'pool_name': '192.168.99.24:/fake/export/path', + 'QoS_support': False, + 'thick_provisioning_support': thick, + 'thin_provisioning_support': not thick, + 'free_capacity_gb': 12.0, + 'total_capacity_gb': 4468.0, + 'reserved_percentage': 7, + 'max_over_subscription_ratio': 19.0, + 'provisioned_capacity_gb': 4456.0}] + + self.assertEqual(expected, result) def test_shortlist_del_eligible_files(self): mock_get_path_for_export = self.mock_object( diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py index 794bb4337..9c855de3b 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/test_nfs_base.py @@ -37,6 +37,7 @@ class NetAppNfsDriverTestCase(test.TestCase): def setUp(self): super(NetAppNfsDriverTestCase, self).setUp() configuration = mock.Mock() + configuration.reserved_percentage = 0 configuration.nfs_mount_point_base = '/mnt/test' configuration.nfs_used_ratio = 0.89 configuration.nfs_oversub_ratio = 3.0 @@ -62,14 +63,10 @@ class NetAppNfsDriverTestCase(test.TestCase): def test_get_share_capacity_info(self): mock_get_capacity = self.mock_object(self.driver, '_get_capacity_info') mock_get_capacity.return_value = fake.CAPACITY_VALUES - expected_total_capacity_gb = (na_utils.round_down( - (fake.TOTAL_BYTES * - self.driver.configuration.nfs_oversub_ratio) / - units.Gi, '0.01')) + expected_total_capacity_gb = na_utils.round_down( + fake.TOTAL_BYTES / units.Gi, '0.01') expected_free_capacity_gb = (na_utils.round_down( - (fake.AVAILABLE_BYTES * - self.driver.configuration.nfs_oversub_ratio) / - units.Gi, '0.01')) + fake.AVAILABLE_BYTES / units.Gi, '0.01')) expected_reserved_percentage = round( 100 * (1 - self.driver.configuration.nfs_used_ratio)) @@ -80,7 +77,7 @@ class NetAppNfsDriverTestCase(test.TestCase): self.assertEqual(expected_free_capacity_gb, result['free_capacity_gb']) self.assertEqual(expected_reserved_percentage, - result['reserved_percentage']) + round(result['reserved_percentage'])) def test_get_capacity_info_ipv4_share(self): expected = fake.CAPACITY_VALUES 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 300cf9f62..4d714fd9b 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 @@ -16,6 +16,7 @@ Mock unit tests for the NetApp cmode nfs storage driver """ +import ddt import mock from os_brick.remotefs import remotefs as remotefs_brick from oslo_service import loopingcall @@ -37,6 +38,7 @@ from cinder.volume.drivers import nfs from cinder.volume import utils as volume_utils +@ddt.ddt class NetAppCmodeNfsDriverTestCase(test.TestCase): def setUp(self): super(NetAppCmodeNfsDriverTestCase, self).setUp() @@ -73,32 +75,90 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): self.assertTrue(mock_check_flags.called) self.assertTrue(mock_super_do_setup.called) - def test_get_pool_stats(self): + @ddt.data({'thin': True, 'nfs_sparsed_volumes': True}, + {'thin': True, 'nfs_sparsed_volumes': False}, + {'thin': False, 'nfs_sparsed_volumes': True}, + {'thin': False, 'nfs_sparsed_volumes': False}) + @ddt.unpack + def test_get_pool_stats(self, thin, nfs_sparsed_volumes): + + class test_volume(object): + pass + + test_volume = test_volume() + test_volume.id = {'vserver': 'openstack', 'name': 'vola'} + test_volume.aggr = { + 'disk_type': 'SSD', + 'ha_policy': 'cfo', + 'junction': '/vola', + 'name': 'aggr1', + 'raid_type': 'raiddp', + } + test_volume.export = {'path': fake.NFS_SHARE} + test_volume.sis = {'dedup': False, 'compression': False} + test_volume.state = { + 'status': 'online', + 'vserver_root': False, + 'junction_active': True, + } + test_volume.qos = {'qos_policy_group': None} + + ssc_map = { + 'mirrored': {}, + 'dedup': {}, + 'compression': {}, + 'thin': {test_volume if thin else None}, + 'all': [test_volume], + } + self.driver.ssc_vols = ssc_map + + self.driver.configuration.nfs_sparsed_volumes = nfs_sparsed_volumes + + netapp_thin = 'true' if thin else 'false' + netapp_thick = 'false' if thin else 'true' + + thick = not thin and not nfs_sparsed_volumes total_capacity_gb = na_utils.round_down( fake.TOTAL_BYTES / units.Gi, '0.01') free_capacity_gb = na_utils.round_down( fake.AVAILABLE_BYTES / units.Gi, '0.01') - capacity = dict( - reserved_percentage = fake.RESERVED_PERCENTAGE, - total_capacity_gb = total_capacity_gb, - free_capacity_gb = free_capacity_gb, - ) - - mock_get_capacity = self.mock_object( - self.driver, '_get_share_capacity_info') - mock_get_capacity.return_value = capacity - - mock_get_vol_for_share = self.mock_object( - self.driver, '_get_vol_for_share') - mock_get_vol_for_share.return_value = None + provisioned_capacity_gb = total_capacity_gb - free_capacity_gb + capacity = { + 'reserved_percentage': fake.RESERVED_PERCENTAGE, + 'max_over_subscription_ratio': fake.MAX_OVER_SUBSCRIPTION_RATIO, + 'total_capacity_gb': total_capacity_gb, + 'free_capacity_gb': free_capacity_gb, + 'provisioned_capacity_gb': provisioned_capacity_gb, + } + self.mock_object(self.driver, + '_get_share_capacity_info', + mock.Mock(return_value=capacity)) result = self.driver._get_pool_stats() - self.assertEqual(fake.RESERVED_PERCENTAGE, - result[0]['reserved_percentage']) - self.assertEqual(total_capacity_gb, result[0]['total_capacity_gb']) - self.assertEqual(free_capacity_gb, result[0]['free_capacity_gb']) + expected = [{'pool_name': '192.168.99.24:/fake/export/path', + 'netapp_unmirrored': 'true', + 'QoS_support': True, + 'thick_provisioning_support': thick, + 'netapp_thick_provisioned': netapp_thick, + 'netapp_nocompression': 'true', + 'thin_provisioning_support': not thick, + 'free_capacity_gb': 12.0, + 'netapp_thin_provisioned': netapp_thin, + 'total_capacity_gb': 4468.0, + 'netapp_compression': 'false', + 'netapp_mirrored': 'false', + 'netapp_dedup': 'false', + 'reserved_percentage': 7, + 'netapp_raid_type': 'raiddp', + 'netapp_disk_type': 'SSD', + 'netapp_nodedup': 'true', + 'reserved_percentage': 7, + 'max_over_subscription_ratio': 19.0, + 'provisioned_capacity_gb': 4456.0}] + + self.assertEqual(expected, result) def test_check_for_setup_error(self): super_check_for_setup_error = self.mock_object( diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py index e2984a06a..1960b6b7f 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py @@ -118,6 +118,10 @@ class NetApp7modeNfsDriver(nfs_base.NetAppNfsDriver): pool['QoS_support'] = False pool.update(capacity) + thick = not self.configuration.nfs_sparsed_volumes + pool['thick_provisioning_support'] = thick + pool['thin_provisioning_support'] = not thick + pools.append(pool) return pools diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_base.py b/cinder/volume/drivers/netapp/dataontap/nfs_base.py index 2b0e0c092..55f35078c 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_base.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_base.py @@ -752,28 +752,16 @@ class NetAppNfsDriver(driver.ManageableVD, def _get_share_capacity_info(self, nfs_share): """Returns the share capacity metrics needed by the scheduler.""" - used_ratio = self.configuration.nfs_used_ratio - oversub_ratio = self.configuration.nfs_oversub_ratio - - # The scheduler's capacity filter will reduce the amount of - # free space that we report to it by the reserved percentage. - reserved_ratio = 1 - used_ratio - reserved_percentage = round(100 * reserved_ratio) - - total_size, total_available = self._get_capacity_info(nfs_share) - - apparent_size = total_size * oversub_ratio - apparent_size_gb = na_utils.round_down( - apparent_size / units.Gi, '0.01') - - apparent_free_size = total_available * oversub_ratio - apparent_free_gb = na_utils.round_down( - float(apparent_free_size) / units.Gi, '0.01') - capacity = dict() - capacity['reserved_percentage'] = reserved_percentage - capacity['total_capacity_gb'] = apparent_size_gb - capacity['free_capacity_gb'] = apparent_free_gb + capacity['reserved_percentage'] = self.reserved_percentage + capacity['max_over_subscription_ratio'] = self.over_subscription_ratio + total_size, total_available = self._get_capacity_info(nfs_share) + capacity['total_capacity_gb'] = na_utils.round_down( + total_size / units.Gi, '0.01') + capacity['free_capacity_gb'] = na_utils.round_down( + total_available / units.Gi, '0.01') + capacity['provisioned_capacity_gb'] = (round( + capacity['total_capacity_gb'] - capacity['free_capacity_gb'], 2)) return capacity diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py index 154eccd73..0dcb6d9d9 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py @@ -197,10 +197,16 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver): pool['netapp_nocompression'] = six.text_type( not compression).lower() - thin = vol in self.ssc_vols['thin'] - pool['netapp_thin_provisioned'] = six.text_type(thin).lower() + flexvol_thin = vol in self.ssc_vols['thin'] + pool['netapp_thin_provisioned'] = six.text_type( + flexvol_thin).lower() pool['netapp_thick_provisioned'] = six.text_type( - not thin).lower() + not flexvol_thin).lower() + + thick = (not flexvol_thin and + not self.configuration.nfs_sparsed_volumes) + pool['thick_provisioning_support'] = thick + pool['thin_provisioning_support'] = not thick pools.append(pool) diff --git a/cinder/volume/drivers/nfs.py b/cinder/volume/drivers/nfs.py index 3f59f5989..52d7a448c 100644 --- a/cinder/volume/drivers/nfs.py +++ b/cinder/volume/drivers/nfs.py @@ -21,6 +21,7 @@ from os_brick.remotefs import remotefs as remotefs_brick from oslo_concurrency import processutils as putils from oslo_config import cfg from oslo_log import log as logging +from oslo_log import versionutils from oslo_utils import units import six @@ -35,6 +36,9 @@ VERSION = '1.3.0' LOG = logging.getLogger(__name__) +NFS_USED_RATIO_DEFAULT = 0.95 +NFS_OVERSUB_RATIO_DEFAULT = 1.0 + nfs_opts = [ cfg.StrOpt('nfs_shares_config', default='/etc/cinder/nfs_shares', @@ -44,16 +48,23 @@ nfs_opts = [ help=('Create volumes as sparsed files which take no space.' 'If set to False volume is created as regular file.' 'In such case volume creation takes a lot of time.')), + # TODO(tbarron): remove nfs_used_ratio in the Mitaka release. cfg.FloatOpt('nfs_used_ratio', - default=0.95, + default=NFS_USED_RATIO_DEFAULT, help=('Percent of ACTUAL usage of the underlying volume ' 'before no new volumes can be allocated to the volume ' - 'destination.')), + 'destination. Note that this option is deprecated ' + 'in favor of "reserved_percentage" and will be removed ' + 'in the Mitaka release.')), + # TODO(tbarron): remove nfs_oversub_ratio in the Mitaka release. cfg.FloatOpt('nfs_oversub_ratio', - default=1.0, + default=NFS_OVERSUB_RATIO_DEFAULT, help=('This will compare the allocated to available space on ' 'the volume destination. If the ratio exceeds this ' - 'number, the destination will no longer be valid.')), + 'number, the destination will no longer be valid. ' + 'Note that this option is deprecated in favor of ' + '"max_oversubscription_ratio" and will be removed ' + 'in the Mitaka release.')), cfg.StrOpt('nfs_mount_point_base', default='$state_path/mnt', help=('Base dir containing mount points for nfs shares.')), @@ -110,6 +121,10 @@ class NfsDriver(driver.ExtendVD, remotefs.RemoteFSDriver): nfs_mount_point_base=self.base, nfs_mount_options=opts) + self._sparse_copy_volume_data = True + self.reserved_percentage = self._get_reserved_percentage() + self.over_subscription_ratio = self._get_over_subscription_ratio() + def set_execute(self, execute): super(NfsDriver, self).set_execute(execute) if self._remotefsclient: @@ -130,18 +145,6 @@ class NfsDriver(driver.ExtendVD, remotefs.RemoteFSDriver): {'config': config}) LOG.warning(msg) raise exception.NfsException(msg) - if not self.configuration.nfs_oversub_ratio > 0: - msg = _("NFS config 'nfs_oversub_ratio' invalid. Must be > 0: " - "%s") % self.configuration.nfs_oversub_ratio - - LOG.error(msg) - raise exception.InvalidConfigurationValue(msg) - if not ((self.configuration.nfs_used_ratio > 0) and - (self.configuration.nfs_used_ratio <= 1)): - msg = _("NFS config 'nfs_used_ratio' invalid. Must be > 0 " - "and <= 1.0: %s") % self.configuration.nfs_used_ratio - LOG.error(msg) - raise exception.InvalidConfigurationValue(msg) self.shares = {} # address : options @@ -232,17 +235,30 @@ class NfsDriver(driver.ExtendVD, remotefs.RemoteFSDriver): :param nfs_share: nfs share :param volume_size_in_gib: int size in GB """ - - used_ratio = self.configuration.nfs_used_ratio - oversub_ratio = self.configuration.nfs_oversub_ratio + # Because the generic NFS driver aggregates over all shares + # when reporting capacity and usage stats to the scheduler, + # we still have to perform some scheduler-like capacity + # checks here, and these have to take into account + # configuration for reserved space and oversubscription. + # It would be better to do all this in the scheduler, but + # this requires either pool support for the generic NFS + # driver or limiting each NFS backend driver to a single share. + + # 'nfs_used_ratio' is deprecated, so derive used_ratio from + # reserved_percentage. + used_percentage = 100 - self.reserved_percentage + used_ratio = used_percentage / 100.0 + + oversub_ratio = self.over_subscription_ratio requested_volume_size = volume_size_in_gib * units.Gi total_size, total_available, total_allocated = \ self._get_capacity_info(nfs_share) apparent_size = max(0, total_size * oversub_ratio) apparent_available = max(0, apparent_size - total_allocated) - used = (total_size - total_available) / total_size - if used > used_ratio: + + actual_used_ratio = (total_size - total_available) / float(total_size) + if actual_used_ratio > used_ratio: # NOTE(morganfainberg): We check the used_ratio first since # with oversubscription it is possible to not have the actual # available space but be within our oversubscription limit @@ -372,3 +388,64 @@ class NfsDriver(driver.ExtendVD, remotefs.RemoteFSDriver): super(NfsDriver, self)._update_volume_stats() self._stats['sparse_copy_volume'] = True + data = self._stats + + global_capacity = data['total_capacity_gb'] + global_free = data['free_capacity_gb'] + + thin_enabled = self.configuration.nfs_sparsed_volumes + if thin_enabled: + provisioned_capacity = self._get_provisioned_capacity() + else: + provisioned_capacity = round(global_capacity - global_free, 2) + + data['provisioned_capacity_gb'] = provisioned_capacity + data['max_over_subscription_ratio'] = self.over_subscription_ratio + data['reserved_percentage'] = self.reserved_percentage + data['thin_provisioning_support'] = thin_enabled + data['thick_provisioning_support'] = not thin_enabled + + self._stats = data + + def _get_over_subscription_ratio(self): + legacy_oversub_ratio = self.configuration.nfs_oversub_ratio + if legacy_oversub_ratio == NFS_OVERSUB_RATIO_DEFAULT: + return self.configuration.max_over_subscription_ratio + + # Honor legacy option if its value is not the default. + msg = _LW("The option 'nfs_oversub_ratio' is deprecated and will " + "be removed in the Mitaka release. Please set " + "'max_over_subscription_ratio = %s' instead.") % ( + self.configuration.nfs_oversub_ratio) + versionutils.report_deprecated_feature(LOG, msg) + + if not self.configuration.nfs_oversub_ratio > 0: + msg = _("NFS config 'nfs_oversub_ratio' invalid. Must be > 0: " + "%s.") % self.configuration.nfs_oversub_ratio + LOG.error(msg) + raise exception.InvalidConfigurationValue(msg) + + return legacy_oversub_ratio + + def _get_reserved_percentage(self): + legacy_used_ratio = self.configuration.nfs_used_ratio + legacy_reserved_ratio = 1 - legacy_used_ratio + legacy_percentage = legacy_reserved_ratio * 100 + if legacy_used_ratio == NFS_USED_RATIO_DEFAULT: + return self.configuration.reserved_percentage + + # Honor legacy option if its value is not the default. + msg = _LW("The option 'nfs_used_ratio' is deprecated and will " + "be removed in the Mitaka release. Please set " + "'reserved_percentage = %d' instead.") % ( + legacy_percentage) + versionutils.report_deprecated_feature(LOG, msg) + + if not ((self.configuration.nfs_used_ratio > 0) and + (self.configuration.nfs_used_ratio <= 1)): + msg = _("NFS config 'nfs_used_ratio' invalid. Must be > 0 " + "and <= 1.0: %s.") % self.configuration.nfs_used_ratio + LOG.error(msg) + raise exception.InvalidConfigurationValue(msg) + + return legacy_percentage -- 2.45.2