Merge "Add manage/unmanage support to NetApp cDOT driver"

This commit is contained in:
Jenkins 2015-09-01 08:46:08 +00:00 committed by Gerrit Code Review
commit 4d8266ed1e
17 changed files with 1290 additions and 22 deletions

View File

@ -916,6 +916,12 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
api_args = {'path': '/vol/%s' % volume_name}
self.send_request('sis-enable', api_args)
@na_utils.trace
def disable_dedup(self, volume_name):
"""Disable deduplication on volume."""
api_args = {'path': '/vol/%s' % volume_name}
self.send_request('sis-disable', api_args)
@na_utils.trace
def enable_compression(self, volume_name):
"""Enable compression on volume."""
@ -925,6 +931,45 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
}
self.send_request('sis-set-config', api_args)
@na_utils.trace
def disable_compression(self, volume_name):
"""Disable compression on volume."""
api_args = {
'path': '/vol/%s' % volume_name,
'enable-compression': 'false'
}
self.send_request('sis-set-config', api_args)
@na_utils.trace
def get_volume_efficiency_status(self, volume_name):
"""Get dedupe & compression status for a volume."""
api_args = {
'query': {
'sis-status-info': {
'path': '/vol/%s' % volume_name,
},
},
'desired-attributes': {
'sis-status-info': {
'state': None,
'is-compression-enabled': None,
},
},
}
result = self.send_request('sis-get-iter', api_args)
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
sis_status_info = attributes_list.get_child_by_name(
'sis-status-info') or netapp_api.NaElement('none')
return {
'dedupe': True if 'enabled' == sis_status_info.get_child_content(
'state') else False,
'compression': True if 'true' == sis_status_info.get_child_content(
'is-compression-enabled') else False,
}
@na_utils.trace
def set_volume_max_files(self, volume_name, max_files):
"""Set flexvol file limit."""
@ -976,6 +1021,78 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
errors[0].get_child_content('error-code'),
errors[0].get_child_content('error-message'))
@na_utils.trace
def set_volume_name(self, volume_name, new_volume_name):
"""Set flexvol name."""
api_args = {
'volume': volume_name,
'new-volume-name': new_volume_name,
}
self.send_request('volume-rename', api_args)
@na_utils.trace
def manage_volume(self, aggregate_name, volume_name,
thin_provisioned=False, snapshot_policy=None,
language=None, dedup_enabled=False,
compression_enabled=False, max_files=None):
"""Update volume as needed to bring under management as a share."""
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': aggregate_name,
'name': volume_name,
},
},
},
'attributes': {
'volume-attributes': {
'volume-inode-attributes': {},
'volume-language-attributes': {},
'volume-snapshot-attributes': {},
'volume-space-attributes': {
'space-guarantee': ('none' if thin_provisioned else
'volume')
},
},
},
}
if language:
api_args['attributes']['volume-attributes'][
'volume-language-attributes']['language'] = language
if max_files:
api_args['attributes']['volume-attributes'][
'volume-inode-attributes']['files-total'] = max_files
if snapshot_policy:
api_args['attributes']['volume-attributes'][
'volume-snapshot-attributes'][
'snapshot-policy'] = snapshot_policy
self.send_request('volume-modify-iter', api_args)
# Efficiency options must be handled separately
self.update_volume_efficiency_attributes(volume_name,
dedup_enabled,
compression_enabled)
@na_utils.trace
def update_volume_efficiency_attributes(self, volume_name, dedup_enabled,
compression_enabled):
"""Update dedupe & compression attributes to match desired values."""
efficiency_status = self.get_volume_efficiency_status(volume_name)
if efficiency_status['compression'] != compression_enabled:
if compression_enabled:
self.enable_compression(volume_name)
else:
self.disable_compression(volume_name)
if efficiency_status['dedupe'] != dedup_enabled:
if dedup_enabled:
self.enable_dedup(volume_name)
else:
self.disable_dedup(volume_name)
@na_utils.trace
def volume_exists(self, volume_name):
"""Checks if volume exists."""
@ -1039,6 +1156,159 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
return aggregate
@na_utils.trace
def volume_has_luns(self, volume_name):
"""Checks if volume has LUNs."""
LOG.debug('Checking if volume %s has LUNs', volume_name)
api_args = {
'query': {
'lun-info': {
'volume': volume_name,
},
},
'desired-attributes': {
'lun-info': {
'path': None,
},
},
}
result = self.send_request('lun-get-iter', api_args)
return self._has_records(result)
@na_utils.trace
def volume_has_junctioned_volumes(self, volume_name):
"""Checks if volume has volumes mounted beneath its junction path."""
junction_path = self.get_volume_junction_path(volume_name)
if not junction_path:
return False
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'junction-path': junction_path + '/*',
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'name': None,
},
},
},
}
result = self.send_request('volume-get-iter', api_args)
return self._has_records(result)
@na_utils.trace
def get_volume_at_junction_path(self, junction_path):
"""Returns the volume with the specified junction path, if present."""
if not junction_path:
return None
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'junction-path': junction_path,
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': None,
'junction-path': None,
'name': None,
'type': None,
'style': None,
},
'volume-space-attributes': {
'size': None,
}
},
},
}
result = self.send_request('volume-get-iter', api_args)
if not self._has_records(result):
return None
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
volume_attributes = attributes_list.get_child_by_name(
'volume-attributes') or netapp_api.NaElement('none')
volume_id_attributes = volume_attributes.get_child_by_name(
'volume-id-attributes') or netapp_api.NaElement('none')
volume_space_attributes = volume_attributes.get_child_by_name(
'volume-space-attributes') or netapp_api.NaElement('none')
volume = {
'aggregate': volume_id_attributes.get_child_content(
'containing-aggregate-name'),
'junction-path': volume_id_attributes.get_child_content(
'junction-path'),
'name': volume_id_attributes.get_child_content('name'),
'type': volume_id_attributes.get_child_content('type'),
'style': volume_id_attributes.get_child_content('style'),
'size': volume_space_attributes.get_child_content('size'),
}
return volume
@na_utils.trace
def get_volume_to_manage(self, aggregate_name, volume_name):
"""Get flexvol to be managed by Manila."""
api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': aggregate_name,
'name': volume_name,
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': None,
'junction-path': None,
'name': None,
'type': None,
'style': None,
},
'volume-space-attributes': {
'size': None,
}
},
},
}
result = self.send_request('volume-get-iter', api_args)
if not self._has_records(result):
return None
attributes_list = result.get_child_by_name(
'attributes-list') or netapp_api.NaElement('none')
volume_attributes = attributes_list.get_child_by_name(
'volume-attributes') or netapp_api.NaElement('none')
volume_id_attributes = volume_attributes.get_child_by_name(
'volume-id-attributes') or netapp_api.NaElement('none')
volume_space_attributes = volume_attributes.get_child_by_name(
'volume-space-attributes') or netapp_api.NaElement('none')
volume = {
'aggregate': volume_id_attributes.get_child_content(
'containing-aggregate-name'),
'junction-path': volume_id_attributes.get_child_content(
'junction-path'),
'name': volume_id_attributes.get_child_content('name'),
'type': volume_id_attributes.get_child_content('type'),
'style': volume_id_attributes.get_child_content('style'),
'size': volume_space_attributes.get_child_content('size'),
}
return volume
@na_utils.trace
def create_volume_clone(self, volume_name, parent_volume_name,
parent_snapshot_name=None):
@ -1067,6 +1337,16 @@ class NetAppCmodeClient(client_base.NetAppBaseClient):
result = self.send_request('volume-get-volume-path', api_args)
return result.get_child_content('junction')
@na_utils.trace
def mount_volume(self, volume_name, junction_path=None):
"""Mounts a volume on a junction path."""
api_args = {
'volume-name': volume_name,
'junction-path': (junction_path if junction_path
else '/%s' % volume_name)
}
self.send_request('volume-mount', api_args)
@na_utils.trace
def offline_volume(self, volume_name):
"""Offlines a volume."""

View File

@ -70,6 +70,12 @@ class NetAppCmodeMultiSvmShareDriver(driver.ShareDriver):
def ensure_share(self, context, share, **kwargs):
pass
def manage_existing(self, share, driver_options):
raise NotImplementedError
def unmanage(self, share):
raise NotImplementedError
def allow_access(self, context, share, access, **kwargs):
self.library.allow_access(context, share, access, **kwargs)

View File

@ -70,6 +70,12 @@ class NetAppCmodeSingleSvmShareDriver(driver.ShareDriver):
def ensure_share(self, context, share, **kwargs):
pass
def manage_existing(self, share, driver_options):
return self.library.manage_existing(share, driver_options)
def unmanage(self, share):
self.library.unmanage(share)
def allow_access(self, context, share, access, **kwargs):
self.library.allow_access(context, share, access, **kwargs)

View File

@ -20,6 +20,7 @@ single-SVM or multi-SVM functionality needed by the cDOT Manila drivers.
"""
import copy
import math
import socket
import time
@ -66,6 +67,7 @@ class NetAppCmodeFileStorageLibrary(object):
self.driver_name = driver_name
self.private_storage = kwargs['private_storage']
self.configuration = kwargs['configuration']
self.configuration.append_config_values(na_opts.netapp_connection_opts)
self.configuration.append_config_values(na_opts.netapp_basicauth_opts)
@ -483,6 +485,23 @@ class NetAppCmodeFileStorageLibrary(object):
result.update(string_args)
return result
@na_utils.trace
def _check_aggregate_extra_specs_validity(self, aggregate_name, specs):
for specs_key in ('netapp_disk_type', 'netapp_raid_type'):
aggr_value = self._ssc_stats.get(aggregate_name, {}).get(specs_key)
specs_value = specs.get(specs_key)
if aggr_value and specs_value and aggr_value != specs_value:
msg = _('Invalid value "%(value)s" for extra_spec "%(key)s" '
'in aggregate %(aggr)s.')
msg_args = {
'value': specs_value,
'key': specs_key,
'aggr': aggregate_name
}
raise exception.NetAppException(msg % msg_args)
@na_utils.trace
def _allocate_container_from_snapshot(self, share, snapshot,
vserver_client):
@ -631,6 +650,103 @@ class NetAppCmodeFileStorageLibrary(object):
raise exception.ShareSnapshotIsBusy(snapshot_name=snapshot_name)
@na_utils.trace
def manage_existing(self, share, driver_options):
vserver, vserver_client = self._get_vserver(share_server=None)
share_size = self._manage_container(share, vserver_client)
export_locations = self._create_export(share, vserver, vserver_client)
return {'size': share_size, 'export_locations': export_locations}
@na_utils.trace
def unmanage(self, share):
pass
@na_utils.trace
def _manage_container(self, share, vserver_client):
"""Bring existing volume under management as a share."""
protocol_helper = self._get_helper(share)
protocol_helper.set_client(vserver_client)
volume_name = protocol_helper.get_share_name_for_share(share)
if not volume_name:
msg = _('Volume could not be determined from export location '
'%(export)s.')
msg_args = {'export': share['export_location']}
raise exception.ManageInvalidShare(reason=msg % msg_args)
share_name = self._get_valid_share_name(share['id'])
aggregate_name = share_utils.extract_host(share['host'], level='pool')
# Get existing volume info
volume = vserver_client.get_volume_to_manage(aggregate_name,
volume_name)
if not volume:
msg = _('Volume %(volume)s not found on aggregate %(aggr)s.')
msg_args = {'volume': volume_name, 'aggr': aggregate_name}
raise exception.ManageInvalidShare(reason=msg % msg_args)
# Ensure volume is manageable
self._validate_volume_for_manage(volume, vserver_client)
# Validate extra specs
extra_specs = share_types.get_extra_specs_from_share(share)
try:
self._check_extra_specs_validity(share, extra_specs)
self._check_aggregate_extra_specs_validity(aggregate_name,
extra_specs)
except exception.ManilaException as ex:
raise exception.ManageExistingShareTypeMismatch(
reason=six.text_type(ex))
provisioning_options = self._get_provisioning_options(extra_specs)
debug_args = {
'share': share_name,
'aggr': aggregate_name,
'options': provisioning_options
}
LOG.debug('Managing share %(share)s on aggregate %(aggr)s with '
'provisioning options %(options)s', debug_args)
# Rename & remount volume on new path
vserver_client.unmount_volume(volume_name)
vserver_client.set_volume_name(volume_name, share_name)
vserver_client.mount_volume(share_name)
# Modify volume to match extra specs
vserver_client.manage_volume(aggregate_name, share_name,
**provisioning_options)
# Save original volume info to private storage
original_data = {
'original_name': volume['name'],
'original_junction_path': volume['junction-path']
}
self.private_storage.update(share['id'], original_data)
# When calculating the size, round up to the next GB.
return int(math.ceil(float(volume['size']) / units.Gi))
@na_utils.trace
def _validate_volume_for_manage(self, volume, vserver_client):
"""Ensure volume is a candidate for becoming a share."""
# Check volume info, extra specs validity
if volume['type'] != 'rw' or volume['style'] != 'flex':
msg = _('Volume %(volume)s must be a read-write flexible volume.')
msg_args = {'volume': volume['name']}
raise exception.ManageInvalidShare(reason=msg % msg_args)
if vserver_client.volume_has_luns(volume['name']):
msg = _('Volume %(volume)s must not contain LUNs.')
msg_args = {'volume': volume['name']}
raise exception.ManageInvalidShare(reason=msg % msg_args)
if vserver_client.volume_has_junctioned_volumes(volume['name']):
msg = _('Volume %(volume)s must not have junctioned volumes.')
msg_args = {'volume': volume['name']}
raise exception.ManageInvalidShare(reason=msg % msg_args)
@na_utils.trace
def extend_share(self, share, new_size, share_server=None):
"""Extends size of existing share."""

View File

@ -47,3 +47,7 @@ class NetAppBaseHelper(object):
@abc.abstractmethod
def get_target(self, share):
"""Returns host where the share located."""
@abc.abstractmethod
def get_share_name_for_share(self, share):
"""Returns the flexvol name that hosts a share."""

View File

@ -88,6 +88,12 @@ class NetAppCmodeCIFSHelper(base.NetAppBaseHelper):
"""Returns OnTap target IP based on share export location."""
return self._get_export_location(share)[0]
@na_utils.trace
def get_share_name_for_share(self, share):
"""Returns the flexvol name that hosts a share."""
_, share_name = self._get_export_location(share)
return share_name
@staticmethod
def _get_export_location(share):
"""Returns host ip and share name for a given CIFS share."""

View File

@ -84,11 +84,18 @@ class NetAppCmodeNFSHelper(base.NetAppBaseHelper):
"""Returns ID of target OnTap device based on export location."""
return self._get_export_location(share)[0]
@na_utils.trace
def get_share_name_for_share(self, share):
"""Returns the flexvol name that hosts a share."""
_, volume_junction_path = self._get_export_location(share)
volume = self._client.get_volume_at_junction_path(volume_junction_path)
return volume.get('name') if volume else None
@staticmethod
def _get_export_location(share):
"""Returns IP address and export location of an NFS share."""
export_location = share['export_location'] or ':'
return export_location.split(':')
return export_location.rsplit(':', 1)
@staticmethod
def _get_export_policy_name(share):

View File

@ -35,10 +35,13 @@ SHARE_AGGREGATE_NAMES = ('fake_aggr1', 'fake_aggr2')
SHARE_AGGREGATE_RAID_TYPES = ('raid4', 'raid_dp')
SHARE_AGGREGATE_DISK_TYPE = 'FCAL'
SHARE_NAME = 'fake_share'
SHARE_SIZE = '1000000000'
SNAPSHOT_NAME = 'fake_snapshot'
PARENT_SHARE_NAME = 'fake_parent_share'
PARENT_SNAPSHOT_NAME = 'fake_parent_snapshot'
MAX_FILES = 5000
LANGUAGE = 'fake_language'
SNAPSHOT_POLICY_NAME = 'fake_snapshot_policy'
EXPORT_POLICY_NAME = 'fake_export_policy'
DELETED_EXPORT_POLICIES = {
VSERVER_NAME: [
@ -1366,3 +1369,76 @@ DELETED_EXPORT_POLICY_GET_ITER_RESPONSE = etree.XML("""
'policy2': DELETED_EXPORT_POLICIES[VSERVER_NAME][1],
'policy3': DELETED_EXPORT_POLICIES[VSERVER_NAME_2][0],
})
LUN_GET_ITER_RESPONSE = etree.XML("""
<results status="passed">
<attributes-list>
<lun-info>
<path>/vol/%(volume)s/fakelun</path>
<qtree />
<volume>%(volume)s</volume>
<vserver>%(vserver)s</vserver>
</lun-info>
</attributes-list>
<num-records>1</num-records>
</results>
""" % {
'vserver': VSERVER_NAME,
'volume': SHARE_NAME,
})
VOLUME_GET_ITER_JUNCTIONED_VOLUMES_RESPONSE = etree.XML("""
<results status="passed">
<attributes-list>
<volume-attributes>
<volume-id-attributes>
<name>fake_volume</name>
<owning-vserver-name>test</owning-vserver-name>
</volume-id-attributes>
</volume-attributes>
</attributes-list>
<num-records>1</num-records>
</results>
""")
VOLUME_GET_ITER_VOLUME_TO_MANAGE_RESPONSE = etree.XML("""
<results status="passed">
<attributes-list>
<volume-attributes>
<volume-id-attributes>
<containing-aggregate-name>%(aggr)s</containing-aggregate-name>
<junction-path>/%(volume)s</junction-path>
<name>%(volume)s</name>
<owning-vserver-name>%(vserver)s</owning-vserver-name>
<style>flex</style>
<type>rw</type>
</volume-id-attributes>
<volume-space-attributes>
<size>%(size)s</size>
</volume-space-attributes>
</volume-attributes>
</attributes-list>
<num-records>1</num-records>
</results>
""" % {
'aggr': SHARE_AGGREGATE_NAME,
'vserver': VSERVER_NAME,
'volume': SHARE_NAME,
'size': SHARE_SIZE,
})
SIS_GET_ITER_RESPONSE = etree.XML("""
<results status="passed">
<attributes-list>
<sis-status-info>
<is-compression-enabled>true</is-compression-enabled>
<path>/vol/%(volume)s</path>
<state>enabled</state>
<vserver>%(vserver)s</vserver>
</sis-status-info>
</attributes-list>
</results>
""" % {
'vserver': VSERVER_NAME,
'volume': SHARE_NAME,
})

View File

@ -1668,6 +1668,96 @@ class NetAppClientCmodeTestCase(test.TestCase):
self.client.enable_dedup.assert_called_once_with(fake.SHARE_NAME)
self.client.enable_compression.assert_called_once_with(fake.SHARE_NAME)
def test_enable_dedup(self):
self.mock_object(self.client, 'send_request')
self.client.enable_dedup(fake.SHARE_NAME)
sis_enable_args = {'path': '/vol/%s' % fake.SHARE_NAME}
self.client.send_request.assert_called_once_with('sis-enable',
sis_enable_args)
def test_disable_dedup(self):
self.mock_object(self.client, 'send_request')
self.client.disable_dedup(fake.SHARE_NAME)
sis_disable_args = {'path': '/vol/%s' % fake.SHARE_NAME}
self.client.send_request.assert_called_once_with('sis-disable',
sis_disable_args)
def test_enable_compression(self):
self.mock_object(self.client, 'send_request')
self.client.enable_compression(fake.SHARE_NAME)
sis_set_config_args = {
'path': '/vol/%s' % fake.SHARE_NAME,
'enable-compression': 'true'
}
self.client.send_request.assert_called_once_with('sis-set-config',
sis_set_config_args)
def test_disable_compression(self):
self.mock_object(self.client, 'send_request')
self.client.disable_compression(fake.SHARE_NAME)
sis_set_config_args = {
'path': '/vol/%s' % fake.SHARE_NAME,
'enable-compression': 'false'
}
self.client.send_request.assert_called_once_with('sis-set-config',
sis_set_config_args)
def test_get_volume_efficiency_status(self):
api_response = netapp_api.NaElement(fake.SIS_GET_ITER_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
result = self.client.get_volume_efficiency_status(fake.SHARE_NAME)
sis_get_iter_args = {
'query': {
'sis-status-info': {
'path': '/vol/%s' % fake.SHARE_NAME,
},
},
'desired-attributes': {
'sis-status-info': {
'state': None,
'is-compression-enabled': None,
},
},
}
self.client.send_request.assert_has_calls([
mock.call('sis-get-iter', sis_get_iter_args)])
expected = {'dedupe': True, 'compression': True}
self.assertDictEqual(expected, result)
def test_get_volume_efficiency_status_not_found(self):
api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
result = self.client.get_volume_efficiency_status(fake.SHARE_NAME)
expected = {'dedupe': False, 'compression': False}
self.assertDictEqual(expected, result)
def test_set_volume_max_files(self):
self.mock_object(self.client, 'send_request')
@ -1694,30 +1784,154 @@ class NetAppClientCmodeTestCase(test.TestCase):
self.client.send_request.assert_called_once_with(
'volume-modify-iter', volume_modify_iter_api_args)
def test_enable_dedup(self):
def test_set_volume_name(self):
self.mock_object(self.client, 'send_request')
self.client.enable_dedup(fake.SHARE_NAME)
self.client.set_volume_name(fake.SHARE_NAME, 'new_name')
sis_enable_args = {'path': '/vol/%s' % fake.SHARE_NAME}
self.client.send_request.assert_called_once_with('sis-enable',
sis_enable_args)
def test_enable_compression(self):
self.mock_object(self.client, 'send_request')
self.client.enable_compression(fake.SHARE_NAME)
sis_set_config_args = {
'path': '/vol/%s' % fake.SHARE_NAME,
'enable-compression': 'true'
volume_rename_api_args = {
'volume': fake.SHARE_NAME,
'new-volume-name': 'new_name',
}
self.client.send_request.assert_called_once_with('sis-set-config',
sis_set_config_args)
self.client.send_request.assert_called_once_with(
'volume-rename', volume_rename_api_args)
def test_manage_volume_no_optional_args(self):
self.mock_object(self.client, 'send_request')
mock_update_volume_efficiency_attributes = self.mock_object(
self.client, 'update_volume_efficiency_attributes')
self.client.manage_volume(fake.SHARE_AGGREGATE_NAME, fake.SHARE_NAME)
volume_modify_iter_api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': fake.SHARE_AGGREGATE_NAME,
'name': fake.SHARE_NAME,
},
},
},
'attributes': {
'volume-attributes': {
'volume-inode-attributes': {},
'volume-language-attributes': {},
'volume-snapshot-attributes': {},
'volume-space-attributes': {
'space-guarantee': 'volume',
},
},
},
}
self.client.send_request.assert_called_once_with(
'volume-modify-iter', volume_modify_iter_api_args)
mock_update_volume_efficiency_attributes.assert_called_once_with(
fake.SHARE_NAME, False, False)
def test_manage_volume_all_optional_args(self):
self.mock_object(self.client, 'send_request')
mock_update_volume_efficiency_attributes = self.mock_object(
self.client, 'update_volume_efficiency_attributes')
self.client.manage_volume(fake.SHARE_AGGREGATE_NAME,
fake.SHARE_NAME,
thin_provisioned=True,
snapshot_policy=fake.SNAPSHOT_POLICY_NAME,
language=fake.LANGUAGE,
dedup_enabled=True,
compression_enabled=False,
max_files=fake.MAX_FILES)
volume_modify_iter_api_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': fake.SHARE_AGGREGATE_NAME,
'name': fake.SHARE_NAME,
},
},
},
'attributes': {
'volume-attributes': {
'volume-inode-attributes': {
'files-total': fake.MAX_FILES,
},
'volume-language-attributes': {
'language': fake.LANGUAGE,
},
'volume-snapshot-attributes': {
'snapshot-policy': fake.SNAPSHOT_POLICY_NAME,
},
'volume-space-attributes': {
'space-guarantee': 'none',
},
},
},
}
self.client.send_request.assert_called_once_with(
'volume-modify-iter', volume_modify_iter_api_args)
mock_update_volume_efficiency_attributes.assert_called_once_with(
fake.SHARE_NAME, True, False)
@ddt.data(
{'existing': (True, True), 'desired': (True, True)},
{'existing': (True, True), 'desired': (False, False)},
{'existing': (True, True), 'desired': (True, False)},
{'existing': (True, False), 'desired': (True, False)},
{'existing': (True, False), 'desired': (False, False)},
{'existing': (True, False), 'desired': (True, True)},
{'existing': (False, False), 'desired': (False, False)},
{'existing': (False, False), 'desired': (True, False)},
{'existing': (False, False), 'desired': (True, True)},
)
@ddt.unpack
def test_update_volume_efficiency_attributes(self, existing, desired):
existing_dedupe = existing[0]
existing_compression = existing[1]
desired_dedupe = desired[0]
desired_compression = desired[1]
self.mock_object(
self.client,
'get_volume_efficiency_status',
mock.Mock(return_value={'dedupe': existing_dedupe,
'compression': existing_compression}))
mock_enable_compression = self.mock_object(self.client,
'enable_compression')
mock_disable_compression = self.mock_object(self.client,
'disable_compression')
mock_enable_dedup = self.mock_object(self.client, 'enable_dedup')
mock_disable_dedup = self.mock_object(self.client, 'disable_dedup')
self.client.update_volume_efficiency_attributes(
fake.SHARE_NAME, desired_dedupe, desired_compression)
if existing_dedupe == desired_dedupe:
self.assertFalse(mock_enable_dedup.called)
self.assertFalse(mock_disable_dedup.called)
elif existing_dedupe and not desired_dedupe:
self.assertFalse(mock_enable_dedup.called)
self.assertTrue(mock_disable_dedup.called)
elif not existing_dedupe and desired_dedupe:
self.assertTrue(mock_enable_dedup.called)
self.assertFalse(mock_disable_dedup.called)
if existing_compression == desired_compression:
self.assertFalse(mock_enable_compression.called)
self.assertFalse(mock_disable_compression.called)
elif existing_compression and not desired_compression:
self.assertFalse(mock_enable_compression.called)
self.assertTrue(mock_disable_compression.called)
elif not existing_compression and desired_compression:
self.assertTrue(mock_enable_compression.called)
self.assertFalse(mock_disable_compression.called)
def test_set_volume_size(self):
@ -1842,6 +2056,227 @@ class NetAppClientCmodeTestCase(test.TestCase):
self.client.get_aggregate_for_volume,
fake.SHARE_NAME)
def test_volume_has_luns(self):
api_response = netapp_api.NaElement(fake.LUN_GET_ITER_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
result = self.client.volume_has_luns(fake.SHARE_NAME)
lun_get_iter_args = {
'query': {
'lun-info': {
'volume': fake.SHARE_NAME,
},
},
'desired-attributes': {
'lun-info': {
'path': None,
},
},
}
self.client.send_request.assert_has_calls([
mock.call('lun-get-iter', lun_get_iter_args)])
self.assertTrue(result)
def test_volume_has_luns_not_found(self):
api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
result = self.client.volume_has_luns(fake.SHARE_NAME)
self.assertFalse(result)
def test_volume_has_junctioned_volumes(self):
api_response = netapp_api.NaElement(
fake.VOLUME_GET_ITER_JUNCTIONED_VOLUMES_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
fake_junction_path = '/%s' % fake.SHARE_NAME
self.mock_object(self.client,
'get_volume_junction_path',
mock.Mock(return_value=fake_junction_path))
result = self.client.volume_has_junctioned_volumes(fake.SHARE_NAME)
volume_get_iter_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'junction-path': fake_junction_path + '/*',
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'name': None,
},
},
},
}
self.client.send_request.assert_has_calls([
mock.call('volume-get-iter', volume_get_iter_args)])
self.assertTrue(result)
def test_volume_has_junctioned_volumes_no_junction_path(self):
self.mock_object(self.client,
'get_volume_junction_path',
mock.Mock(return_value=''))
result = self.client.volume_has_junctioned_volumes(fake.SHARE_NAME)
self.assertFalse(result)
def test_volume_has_junctioned_volumes_not_found(self):
api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
fake_junction_path = '/%s' % fake.SHARE_NAME
self.mock_object(self.client,
'get_volume_junction_path',
mock.Mock(return_value=fake_junction_path))
result = self.client.volume_has_junctioned_volumes(fake.SHARE_NAME)
self.assertFalse(result)
def test_get_volume_at_junction_path(self):
api_response = netapp_api.NaElement(
fake.VOLUME_GET_ITER_VOLUME_TO_MANAGE_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
fake_junction_path = '/%s' % fake.SHARE_NAME
result = self.client.get_volume_at_junction_path(fake_junction_path)
volume_get_iter_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'junction-path': fake_junction_path,
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': None,
'junction-path': None,
'name': None,
'type': None,
'style': None,
},
'volume-space-attributes': {
'size': None,
}
},
},
}
expected = {
'aggregate': fake.SHARE_AGGREGATE_NAME,
'junction-path': fake_junction_path,
'name': fake.SHARE_NAME,
'type': 'rw',
'style': 'flex',
'size': fake.SHARE_SIZE,
}
self.client.send_request.assert_has_calls([
mock.call('volume-get-iter', volume_get_iter_args)])
self.assertDictEqual(expected, result)
def test_get_volume_at_junction_path_not_specified(self):
result = self.client.get_volume_at_junction_path(None)
self.assertIsNone(result)
def test_get_volume_at_junction_path_not_found(self):
api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
fake_junction_path = '/%s' % fake.SHARE_NAME
result = self.client.get_volume_at_junction_path(fake_junction_path)
self.assertIsNone(result)
def test_get_volume_to_manage(self):
api_response = netapp_api.NaElement(
fake.VOLUME_GET_ITER_VOLUME_TO_MANAGE_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
result = self.client.get_volume_to_manage(fake.SHARE_AGGREGATE_NAME,
fake.SHARE_NAME)
volume_get_iter_args = {
'query': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': fake.SHARE_AGGREGATE_NAME,
'name': fake.SHARE_NAME,
},
},
},
'desired-attributes': {
'volume-attributes': {
'volume-id-attributes': {
'containing-aggregate-name': None,
'junction-path': None,
'name': None,
'type': None,
'style': None,
},
'volume-space-attributes': {
'size': None,
}
},
},
}
expected = {
'aggregate': fake.SHARE_AGGREGATE_NAME,
'junction-path': '/%s' % fake.SHARE_NAME,
'name': fake.SHARE_NAME,
'type': 'rw',
'style': 'flex',
'size': fake.SHARE_SIZE,
}
self.client.send_request.assert_has_calls([
mock.call('volume-get-iter', volume_get_iter_args)])
self.assertDictEqual(expected, result)
def test_get_volume_to_manage_not_found(self):
api_response = netapp_api.NaElement(fake.NO_RECORDS_RESPONSE)
self.mock_object(self.client,
'send_request',
mock.Mock(return_value=api_response))
result = self.client.get_volume_to_manage(fake.SHARE_AGGREGATE_NAME,
fake.SHARE_NAME)
self.assertIsNone(result)
def test_create_volume_clone(self):
self.mock_object(self.client, 'send_request')
@ -1910,6 +2345,35 @@ class NetAppClientCmodeTestCase(test.TestCase):
mock.call('volume-get-volume-path', volume_get_volume_path_args)])
self.assertEqual(fake.VOLUME_JUNCTION_PATH_CIFS, result)
def test_mount_volume_default_junction_path(self):
self.mock_object(self.client, 'send_request')
self.client.mount_volume(fake.SHARE_NAME)
volume_mount_args = {
'volume-name': fake.SHARE_NAME,
'junction-path': '/%s' % fake.SHARE_NAME,
}
self.client.send_request.assert_has_calls([
mock.call('volume-mount', volume_mount_args)])
def test_mount_volume(self):
self.mock_object(self.client, 'send_request')
fake_path = '/fake_path'
self.client.mount_volume(fake.SHARE_NAME, junction_path=fake_path)
volume_mount_args = {
'volume-name': fake.SHARE_NAME,
'junction-path': fake_path,
}
self.client.send_request.assert_has_calls([
mock.call('volume-mount', volume_mount_args)])
def test_offline_volume(self):
self.mock_object(self.client, 'send_request')

View File

@ -17,6 +17,7 @@ Unit tests for the NetApp Data ONTAP cDOT base storage driver library.
"""
import copy
import math
import socket
import time
@ -24,6 +25,7 @@ import ddt
import mock
from oslo_log import log
from oslo_service import loopingcall
from oslo_utils import units
from manila import exception
from manila.share.drivers.netapp.dataontap.client import client_cmode
@ -63,6 +65,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
kwargs = {
'configuration': fake.get_config_cmode(),
'private_storage': mock.Mock(),
'app_version': fake.APP_VERSION
}
self.library = lib_base.NetAppCmodeFileStorageLibrary(fake.DRIVER_NAME,
@ -730,6 +733,26 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
fake_share,
vserver_client)
def test_check_aggregate_extra_specs_validity(self):
self.library._have_cluster_creds = True
self.library._ssc_stats = fake.SSC_INFO
result = self.library._check_aggregate_extra_specs_validity(
fake.AGGREGATES[0], fake.EXTRA_SPEC)
self.assertIsNone(result)
def test_check_aggregate_extra_specs_validity_no_match(self):
self.library._have_cluster_creds = True
self.library._ssc_stats = fake.SSC_INFO
self.assertRaises(exception.NetAppException,
self.library._check_aggregate_extra_specs_validity,
fake.AGGREGATES[1],
fake.EXTRA_SPEC)
def test_allocate_container_from_snapshot(self):
vserver_client = mock.Mock()
@ -1086,6 +1109,231 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
mock_sleep.assert_has_calls([mock.call(3)] * 20)
self.assertEqual(20, lib_base.LOG.debug.call_count)
def test_manage_existing(self):
vserver_client = mock.Mock()
self.mock_object(self.library,
'_get_vserver',
mock.Mock(return_value=(fake.VSERVER1,
vserver_client)))
mock_manage_container = self.mock_object(
self.library,
'_manage_container',
mock.Mock(return_value=fake.SHARE_SIZE))
mock_create_export = self.mock_object(
self.library,
'_create_export',
mock.Mock(return_value=fake.NFS_EXPORTS))
result = self.library.manage_existing(fake.SHARE, {})
expected = {
'size': fake.SHARE_SIZE,
'export_locations': fake.NFS_EXPORTS
}
mock_manage_container.assert_called_once_with(fake.SHARE,
vserver_client)
mock_create_export.assert_called_once_with(fake.SHARE,
fake.VSERVER1,
vserver_client)
self.assertDictEqual(expected, result)
def test_unmanage(self):
result = self.library.unmanage(fake.SHARE)
self.assertIsNone(result)
def test_manage_container(self):
vserver_client = mock.Mock()
share_to_manage = copy.deepcopy(fake.SHARE)
share_to_manage['export_location'] = fake.EXPORT_LOCATION
mock_helper = mock.Mock()
mock_helper.get_share_name_for_share.return_value = fake.FLEXVOL_NAME
self.mock_object(self.library,
'_get_helper',
mock.Mock(return_value=mock_helper))
mock_get_volume_to_manage = self.mock_object(
vserver_client,
'get_volume_to_manage',
mock.Mock(return_value=fake.FLEXVOL_TO_MANAGE))
mock_validate_volume_for_manage = self.mock_object(
self.library,
'_validate_volume_for_manage')
self.mock_object(share_types,
'get_extra_specs_from_share',
mock.Mock(return_value=fake.EXTRA_SPEC))
mock_check_extra_specs_validity = self.mock_object(
self.library,
'_check_extra_specs_validity')
mock_check_aggregate_extra_specs_validity = self.mock_object(
self.library,
'_check_aggregate_extra_specs_validity')
result = self.library._manage_container(share_to_manage,
vserver_client)
mock_get_volume_to_manage.assert_called_once_with(
fake.POOL_NAME, fake.FLEXVOL_NAME)
mock_validate_volume_for_manage.assert_called_once_with(
fake.FLEXVOL_TO_MANAGE, vserver_client)
mock_check_extra_specs_validity.assert_called_once_with(
share_to_manage, fake.EXTRA_SPEC)
mock_check_aggregate_extra_specs_validity.assert_called_once_with(
fake.POOL_NAME, fake.EXTRA_SPEC)
vserver_client.unmount_volume.assert_called_once_with(
fake.FLEXVOL_NAME)
vserver_client.set_volume_name.assert_called_once_with(
fake.FLEXVOL_NAME, fake.SHARE_NAME)
vserver_client.mount_volume.assert_called_once_with(
fake.SHARE_NAME)
vserver_client.manage_volume.assert_called_once_with(
fake.POOL_NAME, fake.SHARE_NAME,
**self.library._get_provisioning_options(fake.EXTRA_SPEC))
original_data = {
'original_name': fake.FLEXVOL_TO_MANAGE['name'],
'original_junction_path': fake.FLEXVOL_TO_MANAGE['junction-path'],
}
self.library.private_storage.update.assert_called_once_with(
fake.SHARE['id'], original_data)
expected_size = int(
math.ceil(float(fake.FLEXVOL_TO_MANAGE['size']) / units.Gi))
self.assertEqual(expected_size, result)
def test_manage_container_invalid_export_location(self):
vserver_client = mock.Mock()
share_to_manage = copy.deepcopy(fake.SHARE)
share_to_manage['export_location'] = fake.EXPORT_LOCATION
mock_helper = mock.Mock()
mock_helper.get_share_name_for_share.return_value = None
self.mock_object(self.library,
'_get_helper',
mock.Mock(return_value=mock_helper))
self.assertRaises(exception.ManageInvalidShare,
self.library._manage_container,
share_to_manage,
vserver_client)
def test_manage_container_not_found(self):
vserver_client = mock.Mock()
share_to_manage = copy.deepcopy(fake.SHARE)
share_to_manage['export_location'] = fake.EXPORT_LOCATION
mock_helper = mock.Mock()
mock_helper.get_share_name_for_share.return_value = fake.FLEXVOL_NAME
self.mock_object(self.library,
'_get_helper',
mock.Mock(return_value=mock_helper))
self.mock_object(vserver_client,
'get_volume_to_manage',
mock.Mock(return_value=None))
self.assertRaises(exception.ManageInvalidShare,
self.library._manage_container,
share_to_manage,
vserver_client)
def test_manage_container_invalid_extra_specs(self):
vserver_client = mock.Mock()
share_to_manage = copy.deepcopy(fake.SHARE)
share_to_manage['export_location'] = fake.EXPORT_LOCATION
mock_helper = mock.Mock()
mock_helper.get_share_name_for_share.return_value = fake.FLEXVOL_NAME
self.mock_object(self.library,
'_get_helper',
mock.Mock(return_value=mock_helper))
self.mock_object(vserver_client,
'get_volume_to_manage',
mock.Mock(return_value=fake.FLEXVOL_TO_MANAGE))
self.mock_object(self.library, '_validate_volume_for_manage')
self.mock_object(share_types,
'get_extra_specs_from_share',
mock.Mock(return_value=fake.EXTRA_SPEC))
self.mock_object(self.library,
'_check_extra_specs_validity',
mock.Mock(side_effect=exception.NetAppException))
self.assertRaises(exception.ManageExistingShareTypeMismatch,
self.library._manage_container,
share_to_manage,
vserver_client)
def test_validate_volume_for_manage(self):
vserver_client = mock.Mock()
vserver_client.volume_has_luns = mock.Mock(return_value=False)
vserver_client.volume_has_junctioned_volumes = mock.Mock(
return_value=False)
result = self.library._validate_volume_for_manage(
fake.FLEXVOL_TO_MANAGE, vserver_client)
self.assertIsNone(result)
@ddt.data({
'attribute': 'type',
'value': 'dp',
}, {
'attribute': 'style',
'value': 'infinitevol',
})
@ddt.unpack
def test_validate_volume_for_manage_invalid_volume(self, attribute, value):
flexvol_to_manage = copy.deepcopy(fake.FLEXVOL_TO_MANAGE)
flexvol_to_manage[attribute] = value
vserver_client = mock.Mock()
vserver_client.volume_has_luns = mock.Mock(return_value=False)
vserver_client.volume_has_junctioned_volumes = mock.Mock(
return_value=False)
self.assertRaises(exception.ManageInvalidShare,
self.library._validate_volume_for_manage,
flexvol_to_manage,
vserver_client)
def test_validate_volume_for_manage_luns_present(self):
vserver_client = mock.Mock()
vserver_client.volume_has_luns = mock.Mock(return_value=True)
vserver_client.volume_has_junctioned_volumes = mock.Mock(
return_value=False)
self.assertRaises(exception.ManageInvalidShare,
self.library._validate_volume_for_manage,
fake.FLEXVOL_TO_MANAGE,
vserver_client)
def test_validate_volume_for_manage_junctioned_volumes_present(self):
vserver_client = mock.Mock()
vserver_client.volume_has_luns = mock.Mock(return_value=False)
vserver_client.volume_has_junctioned_volumes = mock.Mock(
return_value=True)
self.assertRaises(exception.ManageInvalidShare,
self.library._validate_volume_for_manage,
fake.FLEXVOL_TO_MANAGE,
vserver_client)
def test_extend_share(self):
vserver_client = mock.Mock()

View File

@ -51,6 +51,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
kwargs = {
'configuration': fake.get_config_cmode(),
'private_storage': mock.Mock(),
'app_version': fake.APP_VERSION
}

View File

@ -44,6 +44,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
kwargs = {
'configuration': config,
'private_storage': mock.Mock(),
'app_version': fake.APP_VERSION
}

View File

@ -31,6 +31,9 @@ VOLUME_NAME_TEMPLATE = 'share_%(share_id)s'
VSERVER_NAME_TEMPLATE = 'os_%s'
AGGREGATE_NAME_SEARCH_PATTERN = '(.*)'
SHARE_NAME = 'share_7cf7c200_d3af_4e05_b87e_9167c95dfcad'
FLEXVOL_NAME = 'fake_volume'
JUNCTION_PATH = '/%s' % FLEXVOL_NAME
EXPORT_LOCATION = '%s:%s' % (HOST_NAME, JUNCTION_PATH)
SNAPSHOT_NAME = 'fake_snapshot'
SHARE_SIZE = 10
TENANT_ID = '24cb2448-13d8-4f41-afd9-eff5c4fd2a57'
@ -73,7 +76,16 @@ SHARE = {
'share_server_id': '7e6a2cc8-871f-4b1d-8364-5aad0f98da86',
'network_info': {
'network_allocations': [{'ip_address': 'ip'}]
}
},
}
FLEXVOL_TO_MANAGE = {
'aggregate': POOL_NAME,
'junction-path': '/%s' % FLEXVOL_NAME,
'name': FLEXVOL_NAME,
'type': 'rw',
'style': 'flex',
'size': '1610612736', # rounds down to 1 GB
}
EXTRA_SPEC = {
@ -83,6 +95,8 @@ EXTRA_SPEC = {
'netapp:dedup': 'True',
'netapp:compression': 'false',
'netapp:max_files': 5000,
'netapp_disk_type': 'FCAL',
'netapp_raid_type': 'raid4',
}
PROVISIONING_OPTIONS = {

View File

@ -45,3 +45,7 @@ USER_ACCESS = {
'access_to': 'fake_user',
'access_level': constants.ACCESS_LEVEL_RW,
}
VOLUME = {
'name': SHARE_NAME,
}

View File

@ -189,6 +189,12 @@ class NetAppClusteredCIFSHelperTestCase(test.TestCase):
target = self.helper.get_target({'export_location': ''})
self.assertEqual('', target)
def test_get_share_name_for_share(self):
share_name = self.helper.get_share_name_for_share(fake.CIFS_SHARE)
self.assertEqual(fake.SHARE_NAME, share_name)
@ddt.data(
{
'location': r'\\%s\%s' % (fake.SHARE_ADDRESS_1, fake.SHARE_NAME),

View File

@ -172,6 +172,27 @@ class NetAppClusteredNFSHelperTestCase(test.TestCase):
target = self.helper.get_target(fake.NFS_SHARE)
self.assertEqual(fake.SHARE_ADDRESS_1, target)
def test_get_share_name_for_share(self):
self.mock_client.get_volume_at_junction_path.return_value = (
fake.VOLUME)
share_name = self.helper.get_share_name_for_share(fake.NFS_SHARE)
self.assertEqual(fake.SHARE_NAME, share_name)
self.mock_client.get_volume_at_junction_path.assert_called_once_with(
fake.NFS_SHARE_PATH)
def test_get_share_name_for_share_not_found(self):
self.mock_client.get_volume_at_junction_path.return_value = None
share_name = self.helper.get_share_name_for_share(fake.NFS_SHARE)
self.assertIsNone(share_name)
self.mock_client.get_volume_at_junction_path.assert_called_once_with(
fake.NFS_SHARE_PATH)
def test_get_target_missing_location(self):
target = self.helper.get_target({'export_location': ''})

View File

@ -124,7 +124,11 @@ class NetAppDriverFactoryTestCase(test.TestCase):
config = na_fakes.create_configuration()
config.local_conf.set_override('driver_handles_share_servers',
mode == na_common.MULTI_SVM)
kwargs = {'configuration': config, 'app_version': 'fake_info'}
kwargs = {
'configuration': config,
'private_storage': mock.Mock(),
'app_version': 'fake_info'
}
driver = na_common.NetAppDriver._create_driver(
family, mode, **kwargs)
@ -136,7 +140,11 @@ class NetAppDriverFactoryTestCase(test.TestCase):
config = na_fakes.create_configuration()
config.local_conf.set_override('driver_handles_share_servers', True)
kwargs = {'configuration': config, 'app_version': 'fake_info'}
kwargs = {
'configuration': config,
'private_storage': mock.Mock(),
'app_version': 'fake_info'
}
driver = na_common.NetAppDriver._create_driver('ONTAP_CLUSTER',
na_common.MULTI_SVM,