import itertools
import os
+import shutil
+import unittest
from lxml import etree
import mock
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
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()
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())
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."""
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)
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
'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})