Add Manage/Unmanage support to NetApp NFS drivers

This change adds support for the Cinder Manage and Unmanage API calls
to the NetApp NFS drivers. This does not add the support for the generic
NFS driver.

Partially-Implements: Blueprint nfs-manage-unmanage

Change-Id: I26eceae948bc151e28bf0c7af5de58cf86bcb605
This commit is contained in:
Glenn M. Gobeli 2015-02-23 04:35:34 -08:00 committed by Tom Barron
parent 35b1a03a45
commit 1f94d9e84d
7 changed files with 468 additions and 1 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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.

View File

@ -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

View File

@ -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.")))

View File

@ -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})

View File

@ -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."""