diff --git a/cinder/tests/test_netapp_nfs.py b/cinder/tests/test_netapp_nfs.py index dfd376d8a7c..8f082d7fd96 100644 --- a/cinder/tests/test_netapp_nfs.py +++ b/cinder/tests/test_netapp_nfs.py @@ -16,6 +16,8 @@ import itertools import os +import shutil +import unittest from lxml import etree import mock @@ -27,6 +29,7 @@ from cinder.i18n import _LW from cinder.image import image_utils from cinder.openstack.common import log as logging from cinder import test +from cinder import utils as cinder_utils from cinder.volume import configuration as conf from cinder.volume.drivers.netapp import common from cinder.volume.drivers.netapp.dataontap.client import api @@ -115,6 +118,13 @@ class FakeResponse(object): class NetAppCmodeNfsDriverTestCase(test.TestCase): """Test direct NetApp C Mode driver.""" + + TEST_NFS_HOST = 'nfs-host1' + TEST_NFS_SHARE_PATH = '/export' + TEST_NFS_EXPORT1 = '%s:%s' % (TEST_NFS_HOST, TEST_NFS_SHARE_PATH) + TEST_NFS_EXPORT2 = 'nfs-host2:/export' + TEST_MNT_POINT = '/mnt/nfs' + def setUp(self): super(NetAppCmodeNfsDriverTestCase, self).setUp() self._custom_setup() @@ -859,6 +869,19 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): configuration.nfs_shares_config = '/nfs' return configuration + @mock.patch.object(utils, 'get_volume_extra_specs') + def test_check_volume_type_mismatch(self, get_specs): + if not hasattr(self._driver, 'vserver'): + return unittest.skip("Test only applies to cmode driver") + get_specs.return_value = {'thin_volume': 'true'} + self._driver._is_share_vol_type_match = mock.Mock(return_value=False) + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self._driver._check_volume_type, 'vol', + 'share', 'file') + get_specs.assert_called_once_with('vol') + self._driver._is_share_vol_type_match.assert_called_once_with( + 'vol', 'share', 'file') + @mock.patch.object(client_base.Client, 'get_ontapi_version', mock.Mock(return_value=(1, 20))) @mock.patch.object(nfs_base.NetAppNfsDriver, 'do_setup', mock.Mock()) @@ -919,6 +942,216 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase): self.assertEqual('446', na_server.get_port()) self.assertEqual('https', na_server.get_transport_type()) + @mock.patch.object(utils, 'get_volume_extra_specs') + def test_check_volume_type_qos(self, get_specs): + get_specs.return_value = {'netapp:qos_policy_group': 'qos'} + self._driver._get_vserver_and_exp_vol = mock.Mock( + return_value=('vs', 'vol')) + self._driver.zapi_client.file_assign_qos = mock.Mock( + side_effect=api.NaApiError) + self._driver._is_share_vol_type_match = mock.Mock(return_value=True) + self.assertRaises(exception.NetAppDriverException, + self._driver._check_volume_type, 'vol', + 'share', 'file') + get_specs.assert_called_once_with('vol') + self.assertEqual(1, + self._driver.zapi_client.file_assign_qos.call_count) + self.assertEqual(1, self._driver._get_vserver_and_exp_vol.call_count) + self._driver._is_share_vol_type_match.assert_called_once_with( + 'vol', 'share') + + @mock.patch.object(utils, 'resolve_hostname', return_value='10.12.142.11') + def test_convert_vol_ref_share_name_to_share_ip(self, mock_hostname): + drv = self._driver + share = "%s/%s" % (self.TEST_NFS_EXPORT1, 'test_file_name') + modified_share = '10.12.142.11:/export/test_file_name' + + modified_vol_ref = drv._convert_vol_ref_share_name_to_share_ip(share) + + self.assertEqual(modified_share, modified_vol_ref) + + @mock.patch.object(utils, 'resolve_hostname', return_value='10.12.142.11') + @mock.patch.object(os.path, 'isfile', return_value=True) + def test_get_share_mount_and_vol_from_vol_ref(self, mock_isfile, + mock_hostname): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + vol_path = "%s/%s" % (self.TEST_NFS_EXPORT1, 'test_file_name') + vol_ref = {'source-name': vol_path} + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + + (share, mount, file_path) = \ + drv._get_share_mount_and_vol_from_vol_ref(vol_ref) + + self.assertEqual(self.TEST_NFS_EXPORT1, share) + self.assertEqual(self.TEST_MNT_POINT, mount) + self.assertEqual('test_file_name', file_path) + + @mock.patch.object(utils, 'resolve_hostname', return_value='10.12.142.11') + def test_get_share_mount_and_vol_from_vol_ref_with_bad_ref(self, + mock_hostname): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + vol_ref = {'source-id': '1234546'} + + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + + self.assertRaises(exception.ManageExistingInvalidReference, + drv._get_share_mount_and_vol_from_vol_ref, vol_ref) + + @mock.patch.object(utils, 'resolve_hostname', return_value='10.12.142.11') + def test_get_share_mount_and_vol_from_vol_ref_where_not_found(self, + mock_host): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + vol_path = "%s/%s" % (self.TEST_NFS_EXPORT2, 'test_file_name') + vol_ref = {'source-name': vol_path} + + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + + self.assertRaises(exception.ManageExistingInvalidReference, + drv._get_share_mount_and_vol_from_vol_ref, vol_ref) + + @mock.patch.object(utils, 'resolve_hostname', return_value='10.12.142.11') + def test_get_share_mount_and_vol_from_vol_ref_where_is_dir(self, + mock_host): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + vol_ref = {'source-name': self.TEST_NFS_EXPORT2} + + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + + self.assertRaises(exception.ManageExistingInvalidReference, + drv._get_share_mount_and_vol_from_vol_ref, vol_ref) + + @mock.patch.object(cinder_utils, 'get_file_size', return_value=1073741824) + def test_manage_existing_get_size(self, get_file_size): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + test_file = 'test_file_name' + volume = FakeVolume() + volume['name'] = 'file-new-managed-123' + volume['id'] = 'volume-new-managed-123' + vol_path = "%s/%s" % (self.TEST_NFS_EXPORT1, test_file) + vol_ref = {'source-name': vol_path} + + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + drv._get_share_mount_and_vol_from_vol_ref = mock.Mock( + return_value=(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, + test_file)) + + vol_size = drv.manage_existing_get_size(volume, vol_ref) + self.assertEqual(1, vol_size) + + @mock.patch.object(cinder_utils, 'get_file_size', return_value=1074253824) + def test_manage_existing_get_size_round_up(self, get_file_size): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + test_file = 'test_file_name' + volume = FakeVolume() + volume['name'] = 'file-new-managed-123' + volume['id'] = 'volume-new-managed-123' + vol_path = "%s/%s" % (self.TEST_NFS_EXPORT1, test_file) + vol_ref = {'source-name': vol_path} + + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + drv._get_share_mount_and_vol_from_vol_ref = mock.Mock( + return_value=(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, + test_file)) + + vol_size = drv.manage_existing_get_size(volume, vol_ref) + self.assertEqual(2, vol_size) + + @mock.patch.object(cinder_utils, 'get_file_size', return_value='badfloat') + def test_manage_existing_get_size_error(self, get_size): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + test_file = 'test_file_name' + volume = FakeVolume() + volume['name'] = 'file-new-managed-123' + volume['id'] = 'volume-new-managed-123' + vol_path = "%s/%s" % (self.TEST_NFS_EXPORT1, test_file) + vol_ref = {'source-name': vol_path} + + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + drv._get_share_mount_and_vol_from_vol_ref = mock.Mock( + return_value=(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, + test_file)) + + self.assertRaises(exception.VolumeBackendAPIException, + drv.manage_existing_get_size, volume, vol_ref) + + @mock.patch.object(cinder_utils, 'get_file_size', return_value=1074253824) + def test_manage_existing(self, get_file_size): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + test_file = 'test_file_name' + volume = FakeVolume() + volume['name'] = 'file-new-managed-123' + volume['id'] = 'volume-new-managed-123' + vol_path = "%s/%s" % (self.TEST_NFS_EXPORT1, test_file) + vol_ref = {'source-name': vol_path} + drv._check_volume_type = mock.Mock() + self.stubs.Set(drv, '_execute', mock.Mock()) + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + drv._get_share_mount_and_vol_from_vol_ref = mock.Mock( + return_value=(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, + test_file)) + shutil.move = mock.Mock() + + location = drv.manage_existing(volume, vol_ref) + self.assertEqual(self.TEST_NFS_EXPORT1, location['provider_location']) + drv._check_volume_type.assert_called_once_with( + volume, self.TEST_NFS_EXPORT1, test_file) + + @mock.patch.object(cinder_utils, 'get_file_size', return_value=1074253824) + def test_manage_existing_move_fails(self, get_file_size): + drv = self._driver + drv._mounted_shares = [self.TEST_NFS_EXPORT1] + test_file = 'test_file_name' + volume = FakeVolume() + volume['name'] = 'volume-new-managed-123' + volume['id'] = 'volume-new-managed-123' + vol_path = "%s/%s" % (self.TEST_NFS_EXPORT1, test_file) + vol_ref = {'source-name': vol_path} + drv._check_volume_type = mock.Mock() + drv._ensure_shares_mounted = mock.Mock() + drv._get_mount_point_for_share = mock.Mock( + return_value=self.TEST_MNT_POINT) + drv._get_share_mount_and_vol_from_vol_ref = mock.Mock( + return_value=(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, + test_file)) + drv._execute = mock.Mock(side_effect=OSError) + self.assertRaises(exception.VolumeBackendAPIException, + drv.manage_existing, volume, vol_ref) + drv._check_volume_type.assert_called_once_with( + volume, self.TEST_NFS_EXPORT1, test_file) + + @mock.patch.object(nfs_base, 'LOG') + def test_unmanage(self, mock_log): + drv = self._driver + volume = FakeVolume() + volume['id'] = '123' + volume['provider_location'] = '/share' + drv.unmanage(volume) + self.assertEqual(1, mock_log.info.call_count) + class NetAppCmodeNfsDriverOnlyTestCase(test.TestCase): """Test direct NetApp C Mode driver only and not inherit.""" @@ -1333,6 +1566,14 @@ class NetApp7modeNfsDriverTestCase(NetAppCmodeNfsDriverTestCase): pool = self._driver.get_pool({'provider_location': 'fake-share'}) self.assertEqual(pool, 'fake-share') + @mock.patch.object(utils, 'get_volume_extra_specs') + def test_check_volume_type_qos(self, get_specs): + get_specs.return_value = {'netapp:qos_policy_group': 'qos'} + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self._driver._check_volume_type, + 'vol', 'share', 'file') + get_specs.assert_called_once_with('vol') + def _set_config(self, configuration): super(NetApp7modeNfsDriverTestCase, self)._set_config( configuration) diff --git a/cinder/tests/test_utils.py b/cinder/tests/test_utils.py index d733a57fae7..21ea0c9c3af 100644 --- a/cinder/tests/test_utils.py +++ b/cinder/tests/test_utils.py @@ -651,6 +651,19 @@ class GetDiskOfPartitionTestCase(test.TestCase): class GetBlkdevMajorMinorTestCase(test.TestCase): + @mock.patch('os.stat') + def test_get_file_size(self, mock_stat): + + class stat_result: + st_mode = 0o777 + st_size = 1074253824 + + test_file = '/var/tmp/made_up_file' + mock_stat.return_value = stat_result + size = utils.get_file_size(test_file) + self.assertEqual(size, stat_result.st_size) + mock_stat.assert_called_once_with(test_file) + @mock.patch('os.stat') def test_get_blkdev_major_minor(self, mock_stat): diff --git a/cinder/utils.py b/cinder/utils.py index c7b17eb2612..378f238e2dd 100644 --- a/cinder/utils.py +++ b/cinder/utils.py @@ -607,6 +607,11 @@ def get_file_gid(path): return os.stat(path).st_gid +def get_file_size(path): + """Returns the file size.""" + return os.stat(path).st_size + + def _get_disk_of_partition(devpath, st=None): """Returns a disk device path from a partition device path, and stat for the device. If devpath is not a partition, devpath is returned as it is. diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index 16fd79caf35..78ba9c1c137 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -937,6 +937,10 @@ class ManageableVD(object): compare against the properties of the referenced backend storage object. If they are incompatible, raise a ManageExistingVolumeTypeMismatch, specifying a reason for the failure. + + :param volume: Cinder volume to manage + :param existing_ref: Driver-specific information used to identify a + volume """ return @@ -945,6 +949,10 @@ class ManageableVD(object): """Return size of volume to be managed by manage_existing. When calculating the size, round up to the next GB. + + :param volume: Cinder volume to manage + :param existing_ref: Driver-specific information used to identify a + volume """ return @@ -958,6 +966,8 @@ class ManageableVD(object): drivers might use this call as an opportunity to clean up any Cinder-specific configuration that they have associated with the backend storage object. + + :param volume: Cinder volume to unmanage """ pass diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py index 34329f753f8..47a8581db87 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_7mode.py @@ -216,3 +216,13 @@ class NetApp7modeNfsDriver(nfs_base.NetAppNfsDriver): def _is_share_vol_compatible(self, volume, share): """Checks if share is compatible with volume to host it.""" return self._is_share_eligible(share, volume['size']) + + def _check_volume_type(self, volume, share, file_name): + """Matches a volume type for share file.""" + extra_specs = na_utils.get_volume_extra_specs(volume) + qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None) \ + if extra_specs else None + if qos_policy_group: + raise exception.ManageExistingVolumeTypeMismatch( + reason=(_("Setting file qos policy group is not supported" + " on this storage family and ontap version."))) diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_base.py b/cinder/volume/drivers/netapp/dataontap/nfs_base.py index 8afa7319d02..24d112ba546 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_base.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_base.py @@ -20,12 +20,15 @@ Volume driver for NetApp NFS storage. """ +import math import os import re +import shutil import threading import time from oslo_concurrency import processutils +from oslo_config import cfg from oslo_utils import excutils from oslo_utils import units import six.moves.urllib.parse as urlparse @@ -679,3 +682,165 @@ class NetAppNfsDriver(nfs.NfsDriver): 'subscribed_ratio': subscribed_ratio, 'apparent_size': apparent_size, 'apparent_available': apparent_available} + + def _check_volume_type(self, volume, share, file_name): + """Match volume type for share file.""" + raise NotImplementedError() + + def _convert_vol_ref_share_name_to_share_ip(self, vol_ref): + """Converts the share point name to an IP address + + The volume reference may have a DNS name portion in the share name. + Convert that to an IP address and then restore the entire path. + + :param vol_ref: Driver-specific information used to identify a volume + :return: A volume reference where share is in IP format. + """ + # First strip out share and convert to IP format. + share_split = vol_ref.rsplit(':', 1) + + vol_ref_share_ip = na_utils.resolve_hostname(share_split[0]) + + # Now place back into volume reference. + vol_ref_share = vol_ref_share_ip + ':' + share_split[1] + + return vol_ref_share + + def _get_share_mount_and_vol_from_vol_ref(self, vol_ref): + """Get the NFS share, the NFS mount, and the volume from reference + + Determine the NFS share point, the NFS mount point, and the volume + (with possible path) from the given volume reference. Raise exception + if unsuccessful. + + :param vol_ref: Driver-specific information used to identify a volume + :return: NFS Share, NFS mount, volume path or raise error + """ + # Check that the reference is valid. + if 'source-name' not in vol_ref: + reason = _('Reference must contain source-name element.') + raise exception.ManageExistingInvalidReference( + existing_ref=vol_ref, reason=reason) + vol_ref_name = vol_ref['source-name'] + + self._ensure_shares_mounted() + + # If a share was declared as '1.2.3.4:/a/b/c' in the nfs_shares_config + # file, but the admin tries to manage the file located at + # 'my.hostname.com:/a/b/c/d.vol', this might cause a lookup miss below + # when searching self._mounted_shares to see if we have an existing + # mount that would work to access the volume-to-be-managed (a string + # comparison is done instead of IP comparison). + vol_ref_share = self._convert_vol_ref_share_name_to_share_ip( + vol_ref_name) + for nfs_share in self._mounted_shares: + cfg_share = self._convert_vol_ref_share_name_to_share_ip(nfs_share) + (orig_share, work_share, file_path) = \ + vol_ref_share.partition(cfg_share) + if work_share == cfg_share: + file_path = file_path[1:] # strip off leading path divider + LOG.debug("Found possible share %s; checking mount.", + work_share) + nfs_mount = self._get_mount_point_for_share(nfs_share) + vol_full_path = os.path.join(nfs_mount, file_path) + if os.path.isfile(vol_full_path): + LOG.debug("Found share %(share)s and vol %(path)s on " + "mount %(mnt)s", + {'share': nfs_share, 'path': file_path, + 'mnt': nfs_mount}) + return nfs_share, nfs_mount, file_path + else: + LOG.debug("vol_ref %(ref)s not on share %(share)s.", + {'ref': vol_ref_share, 'share': nfs_share}) + + raise exception.ManageExistingInvalidReference( + existing_ref=vol_ref, + reason=_('Volume not found on configured storage backend.')) + + def manage_existing(self, volume, existing_vol_ref): + """Manages an existing volume. + + The specified Cinder volume is to be taken into Cinder management. + The driver will verify its existence and then rename it to the + new Cinder volume name. It is expected that the existing volume + reference is an NFS share point and some [/path]/volume; + e.g., 10.10.32.1:/openstack/vol_to_manage + or 10.10.32.1:/openstack/some_directory/vol_to_manage + + :param volume: Cinder volume to manage + :param existing_vol_ref: Driver-specific information used to identify a + volume + """ + # Attempt to find NFS share, NFS mount, and volume path from vol_ref. + (nfs_share, nfs_mount, vol_path) = \ + self._get_share_mount_and_vol_from_vol_ref(existing_vol_ref) + + LOG.debug("Asked to manage NFS volume %(vol)s, with vol ref %(ref)s", + {'vol': volume['id'], + 'ref': existing_vol_ref['source-name']}) + self._check_volume_type(volume, nfs_share, vol_path) + if vol_path == volume['name']: + LOG.debug("New Cinder volume %s name matches reference name: " + "no need to rename.", volume['name']) + else: + src_vol = os.path.join(nfs_mount, vol_path) + dst_vol = os.path.join(nfs_mount, volume['name']) + try: + shutil.move(src_vol, dst_vol) + LOG.debug("Setting newly managed Cinder volume name to %s", + volume['name']) + self._set_rw_permissions_for_all(dst_vol) + except (OSError, IOError) as err: + exception_msg = (_("Failed to manage existing volume %(name)s," + " because rename operation failed:" + " Error msg: %(msg)s."), + {'name': existing_vol_ref['source-name'], + 'msg': err}) + raise exception.VolumeBackendAPIException(data=exception_msg) + return {'provider_location': nfs_share} + + def manage_existing_get_size(self, volume, existing_vol_ref): + """Returns the size of volume to be managed by manage_existing. + + When calculating the size, round up to the next GB. + + :param volume: Cinder volume to manage + :param existing_vol_ref: Existing volume to take under management + """ + # Attempt to find NFS share, NFS mount, and volume path from vol_ref. + (nfs_share, nfs_mount, vol_path) = \ + self._get_share_mount_and_vol_from_vol_ref(existing_vol_ref) + + try: + LOG.debug("Asked to get size of NFS vol_ref %s.", + existing_vol_ref['source-name']) + + file_path = os.path.join(nfs_mount, vol_path) + file_size = float(utils.get_file_size(file_path)) / units.Gi + vol_size = int(math.ceil(file_size)) + except (OSError, ValueError): + exception_message = (_("Failed to manage existing volume " + "%(name)s, because of error in getting " + "volume size."), + {'name': existing_vol_ref['source-name']}) + raise exception.VolumeBackendAPIException(data=exception_message) + + LOG.debug("Reporting size of NFS volume ref %(ref)s as %(size)d GB.", + {'ref': existing_vol_ref['source-name'], 'size': vol_size}) + + return vol_size + + def unmanage(self, volume): + """Removes the specified volume from Cinder management. + + Does not delete the underlying backend storage object. A log entry + will be made to notify the Admin that the volume is no longer being + managed. + + :param volume: Cinder volume to unmanage + """ + CONF = cfg.CONF + vol_str = CONF.volume_name_template % volume['id'] + vol_path = os.path.join(volume['provider_location'], vol_str) + LOG.info(_LI("Cinder NFS volume with current path \"%(cr)s\" is " + "no longer being managed."), {'cr': vol_path}) diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py index a9e5bbf311e..21c8aca6ad0 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_cmode.py @@ -31,6 +31,7 @@ from cinder.i18n import _, _LE, _LI, _LW from cinder.image import image_utils from cinder.openstack.common import log as logging from cinder import utils +from cinder.volume.drivers.netapp.dataontap.client import api as na_api from cinder.volume.drivers.netapp.dataontap.client import client_cmode from cinder.volume.drivers.netapp.dataontap import nfs_base from cinder.volume.drivers.netapp.dataontap import ssc_cmode @@ -106,7 +107,7 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver): qos_policy_group) return {'provider_location': volume['provider_location']} except Exception as ex: - LOG.error(_LW("Exception creattest_nfs.pying vol %(name)s on " + LOG.error(_LW("Exception creating vol %(name)s on " "share %(share)s. Details: %(ex)s") % {'name': volume['name'], 'share': volume['provider_location'], @@ -128,6 +129,28 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver): qos_policy_group, target_path) + def _check_volume_type(self, volume, share, file_name): + """Match volume type for share file.""" + extra_specs = na_utils.get_volume_extra_specs(volume) + qos_policy_group = extra_specs.pop('netapp:qos_policy_group', None) \ + if extra_specs else None + if not self._is_share_vol_type_match(volume, share): + raise exception.ManageExistingVolumeTypeMismatch( + reason=(_("Volume type does not match for share %s."), + share)) + if qos_policy_group: + try: + vserver, flex_vol_name = self._get_vserver_and_exp_vol( + share=share) + self.zapi_client.file_assign_qos(flex_vol_name, + qos_policy_group, + file_name) + except na_api.NaApiError as ex: + LOG.exception(_LE('Setting file QoS policy group failed. %s'), + ex) + raise exception.NetAppDriverException( + reason=(_('Setting file QoS policy group failed. %s'), ex)) + def _clone_volume(self, volume_name, clone_name, volume_id, share=None): """Clones mounted volume on NetApp Cluster."""