diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py
index 07bc08d069c..14ec67962bc 100644
--- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py
+++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py
@@ -663,6 +663,31 @@ AGGR_GET_ITER_CAPACITY_RESPONSE = etree.XML("""
'total_size': AGGR_SIZE_TOTAL,
})
+VOLUME_STATE_ONLINE = 'online'
+VOLUME_GET_ITER_STATE_ATTR_STR = """
+
+
+ flexgroup
+
+
+ %(state)s
+
+
+""" % {
+ 'state': VOLUME_STATE_ONLINE
+}
+
+VOLUME_GET_ITER_STATE_ATTR = etree.XML(VOLUME_GET_ITER_STATE_ATTR_STR)
+
+VOLUME_GET_ITER_STATE_RESPONSE = etree.XML("""
+
+ 1
+ %(volume)s
+
+""" % {
+ 'volume': VOLUME_GET_ITER_STATE_ATTR_STR,
+})
+
VOLUME_SIZE_TOTAL = 19922944
VOLUME_SIZE_AVAILABLE = 19791872
VOLUME_GET_ITER_CAPACITY_ATTR_STR = """
diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py
index d92002a1020..7da3c63efde 100644
--- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py
+++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py
@@ -22,6 +22,7 @@ import uuid
import ddt
from lxml import etree
+from oslo_utils import units
import paramiko
import six
@@ -1717,6 +1718,51 @@ class NetAppCmodeClientTestCase(test.TestCase):
self.assertEqual(expected_result, address_list)
+ @ddt.data({'junction_path': '/fake/vol'},
+ {'name': 'fake_volume'},
+ {'junction_path': '/fake/vol', 'name': 'fake_volume'})
+ def test_get_volume_state(self, kwargs):
+
+ api_response = netapp_api.NaElement(
+ fake_client.VOLUME_GET_ITER_STATE_RESPONSE)
+ mock_send_iter_request = self.mock_object(
+ self.client, 'send_iter_request', return_value=api_response)
+ volume_response = netapp_api.NaElement(
+ fake_client.VOLUME_GET_ITER_STATE_ATTR)
+ mock_get_unique_vol = self.mock_object(
+ self.client, 'get_unique_volume', return_value=volume_response)
+
+ state = self.client.get_volume_state(**kwargs)
+
+ volume_id_attributes = {}
+ if 'junction_path' in kwargs:
+ volume_id_attributes['junction-path'] = kwargs['junction_path']
+ if 'name' in kwargs:
+ volume_id_attributes['name'] = kwargs['name']
+
+ volume_get_iter_args = {
+ 'query': {
+ 'volume-attributes': {
+ 'volume-id-attributes': volume_id_attributes,
+ }
+ },
+ 'desired-attributes': {
+ 'volume-attributes': {
+ 'volume-id-attributes': {
+ 'style-extended': None,
+ },
+ 'volume-state-attributes': {
+ 'state': None
+ }
+ }
+ },
+ }
+ mock_send_iter_request.assert_called_once_with(
+ 'volume-get-iter', volume_get_iter_args)
+ mock_get_unique_vol.assert_called_once_with(api_response)
+
+ self.assertEqual(fake_client.VOLUME_STATE_ONLINE, state)
+
@ddt.data({'flexvol_path': '/fake/vol'},
{'flexvol_name': 'fake_volume'},
{'flexvol_path': '/fake/vol', 'flexvol_name': 'fake_volume'})
@@ -1961,6 +2007,51 @@ class NetAppCmodeClientTestCase(test.TestCase):
self.client.enable_flexvol_compression.assert_called_once_with(
fake_client.VOLUME_NAME)
+ def test_create_volume_async(self):
+ self.mock_object(self.client.connection, 'send_request')
+
+ self.client.create_volume_async(
+ fake_client.VOLUME_NAME, [fake_client.VOLUME_AGGREGATE_NAME], 100,
+ volume_type='dp')
+
+ volume_create_args = {
+ 'aggr-list': [{'aggr-name': fake_client.VOLUME_AGGREGATE_NAME}],
+ 'size': 100 * units.Gi,
+ 'volume-name': fake_client.VOLUME_NAME,
+ 'volume-type': 'dp'
+ }
+
+ self.client.connection.send_request.assert_called_once_with(
+ 'volume-create-async', volume_create_args)
+
+ @ddt.data('dp', 'rw', None)
+ def test_create_volume_async_with_extra_specs(self, volume_type):
+ self.mock_object(self.client.connection, 'send_request')
+
+ self.client.create_volume_async(
+ fake_client.VOLUME_NAME, [fake_client.VOLUME_AGGREGATE_NAME], 100,
+ space_guarantee_type='volume', language='en-US',
+ snapshot_policy='default', snapshot_reserve=15,
+ volume_type=volume_type)
+
+ volume_create_args = {
+ 'aggr-list': [{'aggr-name': fake_client.VOLUME_AGGREGATE_NAME}],
+ 'size': 100 * units.Gi,
+ 'volume-name': fake_client.VOLUME_NAME,
+ 'space-reserve': 'volume',
+ 'language-code': 'en-US',
+ 'volume-type': volume_type,
+ 'percentage-snapshot-reserve': '15',
+ }
+
+ if volume_type != 'dp':
+ volume_create_args['snapshot-policy'] = 'default'
+ volume_create_args['junction-path'] = ('/%s' %
+ fake_client.VOLUME_NAME)
+
+ self.client.connection.send_request.assert_called_with(
+ 'volume-create-async', volume_create_args)
+
def test_flexvol_exists(self):
api_response = netapp_api.NaElement(
@@ -2045,6 +2136,53 @@ class NetAppCmodeClientTestCase(test.TestCase):
self.client.connection.send_request.assert_has_calls([
mock.call('volume-mount', volume_mount_args)])
+ def test_enable_volume_dedupe_async(self):
+ self.mock_object(self.client.connection, 'send_request')
+
+ self.client.enable_volume_dedupe_async(fake_client.VOLUME_NAME)
+
+ sis_enable_args = {'volume-name': fake_client.VOLUME_NAME}
+
+ self.client.connection.send_request.assert_called_once_with(
+ 'sis-enable-async', sis_enable_args)
+
+ def test_disable_volume_dedupe_async(self):
+
+ self.mock_object(self.client.connection, 'send_request')
+
+ self.client.disable_volume_dedupe_async(fake_client.VOLUME_NAME)
+
+ sis_enable_args = {'volume-name': fake_client.VOLUME_NAME}
+
+ self.client.connection.send_request.assert_called_once_with(
+ 'sis-disable-async', sis_enable_args)
+
+ def test_enable_volume_compression_async(self):
+ self.mock_object(self.client.connection, 'send_request')
+
+ self.client.enable_volume_compression_async(fake_client.VOLUME_NAME)
+
+ sis_set_config_args = {
+ 'volume-name': fake_client.VOLUME_NAME,
+ 'enable-compression': 'true'
+ }
+
+ self.client.connection.send_request.assert_called_once_with(
+ 'sis-set-config-async', sis_set_config_args)
+
+ def test_disable_volume_compression_async(self):
+ self.mock_object(self.client.connection, 'send_request')
+
+ self.client.disable_volume_compression_async(fake_client.VOLUME_NAME)
+
+ sis_set_config_args = {
+ 'volume-name': fake_client.VOLUME_NAME,
+ 'enable-compression': 'false'
+ }
+
+ self.client.connection.send_request.assert_called_once_with(
+ 'sis-set-config-async', sis_set_config_args)
+
def test_enable_flexvol_dedupe(self):
self.mock_object(self.client.connection, 'send_request')
diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py
index 69db742ebbd..b5c1a990c3e 100644
--- a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py
+++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py
@@ -135,12 +135,12 @@ SSC_AGGREGATE_INFO = {
}
PROVISIONING_OPTS = {
- 'aggregate': 'fake_aggregate',
+ 'aggregate': ['fake_aggregate'],
'thin_provisioned': True,
'snapshot_policy': None,
'language': 'en_US',
- 'dedupe_enabled': False,
- 'compression_enabled': False,
+ 'dedupe_enabled': True,
+ 'compression_enabled': True,
'snapshot_reserve': '12',
'volume_type': 'rw',
'size': 20,
@@ -148,7 +148,7 @@ PROVISIONING_OPTS = {
}
ENCRYPTED_PROVISIONING_OPTS = {
- 'aggregate': 'fake_aggregate',
+ 'aggregate': ['fake_aggregate'],
'thin_provisioned': True,
'snapshot_policy': None,
'language': 'en_US',
diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_data_motion.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_data_motion.py
index 6f4a45ccac0..f332be34303 100644
--- a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_data_motion.py
+++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_data_motion.py
@@ -166,9 +166,15 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
self.assertDictEqual({'aggr1': 'aggr10'}, aggr_map)
- @ddt.data(True, False)
- def test_create_snapmirror_dest_flexvol_exists(self, dest_exists):
+ @ddt.data({'dest_exists': True, 'is_flexgroup': False},
+ {'dest_exists': True, 'is_flexgroup': True},
+ {'dest_exists': False, 'is_flexgroup': False},
+ {'dest_exists': False, 'is_flexgroup': True})
+ @ddt.unpack
+ def test_create_snapmirror_dest_flexvol_exists(self, dest_exists,
+ is_flexgroup):
mock_dest_client = mock.Mock()
+ mock_src_client = mock.Mock()
self.mock_object(mock_dest_client, 'flexvol_exists',
return_value=dest_exists)
self.mock_object(mock_dest_client, 'get_snapmirrors',
@@ -176,7 +182,15 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
create_destination_flexvol = self.mock_object(
self.dm_mixin, 'create_destination_flexvol')
self.mock_object(utils, 'get_client_for_backend',
- return_value=mock_dest_client)
+ side_effect=[mock_dest_client,
+ mock_src_client])
+
+ mock_provisioning_options = mock.Mock()
+ mock_provisioning_options.get.return_value = is_flexgroup
+
+ self.mock_object(mock_src_client,
+ 'get_provisioning_options_from_flexvol',
+ return_value=mock_provisioning_options)
self.dm_mixin.create_snapmirror(self.src_backend,
self.dest_backend,
@@ -186,16 +200,72 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
if not dest_exists:
create_destination_flexvol.assert_called_once_with(
self.src_backend, self.dest_backend, self.src_flexvol_name,
- self.dest_flexvol_name)
+ self.dest_flexvol_name, pool_is_flexgroup=is_flexgroup)
else:
self.assertFalse(create_destination_flexvol.called)
mock_dest_client.create_snapmirror.assert_called_once_with(
self.src_vserver, self.src_flexvol_name, self.dest_vserver,
- self.dest_flexvol_name, schedule='hourly')
+ self.dest_flexvol_name,
+ schedule='hourly',
+ relationship_type=('extended_data_protection'
+ if is_flexgroup
+ else 'data_protection'))
mock_dest_client.initialize_snapmirror.assert_called_once_with(
self.src_vserver, self.src_flexvol_name, self.dest_vserver,
self.dest_flexvol_name)
+ def test_create_snapmirror_cleanup_on_geometry_has_changed(self):
+ mock_dest_client = mock.Mock()
+ mock_src_client = mock.Mock()
+ self.mock_object(mock_dest_client, 'flexvol_exists',
+ return_value=True)
+ self.mock_object(mock_dest_client, 'get_snapmirrors',
+ return_value=None)
+ create_destination_flexvol = self.mock_object(
+ self.dm_mixin, 'create_destination_flexvol')
+ mock_delete_snapshot = self.mock_object(
+ self.dm_mixin, 'delete_snapmirror'
+ )
+ self.mock_object(utils, 'get_client_for_backend',
+ side_effect=[mock_dest_client,
+ mock_src_client])
+
+ geometry_exception_message = ("Geometry of the destination FlexGroup "
+ "has been changed since the SnapMirror "
+ "relationship was created.")
+ mock_dest_client.initialize_snapmirror.side_effect = [
+ netapp_api.NaApiError(code=netapp_api.EAPIERROR,
+ message=geometry_exception_message),
+ ]
+
+ mock_provisioning_options = mock.Mock()
+ mock_provisioning_options.get.return_value = False
+
+ self.mock_object(mock_src_client,
+ 'get_provisioning_options_from_flexvol',
+ return_value=mock_provisioning_options)
+
+ self.assertRaises(na_utils.GeometryHasChangedOnDestination,
+ self.dm_mixin.create_snapmirror,
+ self.src_backend,
+ self.dest_backend,
+ self.src_flexvol_name,
+ self.dest_flexvol_name)
+
+ self.assertFalse(create_destination_flexvol.called)
+ mock_dest_client.create_snapmirror.assert_called_once_with(
+ self.src_vserver, self.src_flexvol_name, self.dest_vserver,
+ self.dest_flexvol_name, schedule='hourly',
+ relationship_type='data_protection')
+
+ mock_dest_client.initialize_snapmirror.assert_called_once_with(
+ self.src_vserver, self.src_flexvol_name, self.dest_vserver,
+ self.dest_flexvol_name)
+
+ mock_delete_snapshot.assert_called_once_with(
+ self.src_backend, self.dest_backend, self.src_flexvol_name,
+ self.dest_flexvol_name)
+
@ddt.data('uninitialized', 'broken-off', 'snapmirrored')
def test_create_snapmirror_snapmirror_exists_state(self, mirror_state):
mock_dest_client = mock.Mock()
@@ -223,7 +293,7 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
mock_dest_client.resume_snapmirror.assert_called_once_with(
self.src_vserver, self.src_flexvol_name,
self.dest_vserver, self.dest_flexvol_name)
- mock_dest_client.resume_snapmirror.assert_called_once_with(
+ mock_dest_client.resync_snapmirror.assert_called_once_with(
self.src_vserver, self.src_flexvol_name,
self.dest_vserver, self.dest_flexvol_name)
@@ -254,9 +324,10 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
mock_dest_client.resume_snapmirror.assert_called_once_with(
self.src_vserver, self.src_flexvol_name,
self.dest_vserver, self.dest_flexvol_name)
- mock_dest_client.resume_snapmirror.assert_called_once_with(
- self.src_vserver, self.src_flexvol_name,
- self.dest_vserver, self.dest_flexvol_name)
+ if failed_call == 'resync_snapmirror':
+ mock_dest_client.resync_snapmirror.assert_called_once_with(
+ self.src_vserver, self.src_flexvol_name,
+ self.dest_vserver, self.dest_flexvol_name)
self.assertEqual(1, mock_exception_log.call_count)
def test_delete_snapmirror(self):
@@ -528,6 +599,7 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
self.dm_mixin.create_destination_flexvol,
self.src_backend, self.dest_backend,
self.src_flexvol_name, self.dest_flexvol_name)
+
if size and is_flexgroup is False:
self.dm_mixin._get_replication_aggregate_map.\
assert_called_once_with(self.src_backend, self.dest_backend)
@@ -536,9 +608,63 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
self.dm_mixin._get_replication_aggregate_map.called)
self.assertFalse(mock_client_call.called)
- def test_create_destination_flexvol(self):
+ @ddt.data('mixed', None)
+ def test_create_destination_flexgroup_online_timeout(self, volume_state):
aggr_map = {
- fakes.PROVISIONING_OPTS['aggregate']: 'aggr01',
+ fakes.PROVISIONING_OPTS['aggregate'][0]: 'aggr01',
+ 'aggr20': 'aggr02',
+ }
+ provisioning_opts = copy.deepcopy(fakes.PROVISIONING_OPTS)
+ expected_prov_opts = copy.deepcopy(fakes.PROVISIONING_OPTS)
+ expected_prov_opts.pop('volume_type', None)
+ expected_prov_opts.pop('size', None)
+ expected_prov_opts.pop('aggregate', None)
+ expected_prov_opts.pop('is_flexgroup', None)
+
+ self.mock_object(
+ self.mock_src_client, 'get_provisioning_options_from_flexvol',
+ return_value=provisioning_opts)
+ self.mock_object(self.dm_mixin, '_get_replication_aggregate_map',
+ return_value=aggr_map)
+ self.mock_object(self.dm_mixin,
+ '_get_replication_volume_online_timeout',
+ return_value=2)
+
+ mock_create_volume_async = self.mock_object(self.mock_dest_client,
+ 'create_volume_async')
+ mock_volume_state = self.mock_object(self.mock_dest_client,
+ 'get_volume_state',
+ return_value=volume_state)
+ self.mock_object(self.mock_src_client, 'is_flexvol_encrypted',
+ return_value=False)
+
+ mock_dedupe_enabled = self.mock_object(
+ self.mock_dest_client, 'enable_volume_dedupe_async')
+ mock_compression_enabled = self.mock_object(
+ self.mock_dest_client, 'enable_volume_compression_async')
+
+ self.assertRaises(na_utils.NetAppDriverException,
+ self.dm_mixin.create_destination_flexvol,
+ self.src_backend, self.dest_backend,
+ self.src_flexvol_name, self.dest_flexvol_name,
+ pool_is_flexgroup=True)
+
+ expected_prov_opts.pop('dedupe_enabled')
+ expected_prov_opts.pop('compression_enabled')
+ mock_create_volume_async.assert_called_once_with(
+ self.dest_flexvol_name,
+ ['aggr01'],
+ fakes.PROVISIONING_OPTS['size'],
+ volume_type='dp', **expected_prov_opts)
+ mock_volume_state.assert_called_with(
+ flexvol_name=self.dest_flexvol_name)
+ mock_dedupe_enabled.assert_not_called()
+ mock_compression_enabled.assert_not_called()
+
+ @ddt.data('flexvol', 'flexgroup')
+ def test_create_destination_flexvol(self, volume_style):
+ aggr_map = {
+ fakes.PROVISIONING_OPTS['aggregate'][0]: 'aggr01',
'aggr20': 'aggr02',
}
provisioning_opts = copy.deepcopy(fakes.PROVISIONING_OPTS)
@@ -555,27 +681,64 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
return_value=False)
self.mock_object(self.dm_mixin, '_get_replication_aggregate_map',
return_value=aggr_map)
- mock_client_call = self.mock_object(
- self.mock_dest_client, 'create_flexvol')
+
+ pool_is_flexgroup = False
+ if volume_style == 'flexgroup':
+ pool_is_flexgroup = True
+ self.mock_object(self.dm_mixin,
+ '_get_replication_volume_online_timeout',
+ return_value=2)
+ mock_create_volume_async = self.mock_object(self.mock_dest_client,
+ 'create_volume_async')
+ mock_volume_state = self.mock_object(self.mock_dest_client,
+ 'get_volume_state',
+ return_value='online')
+ mock_dedupe_enabled = self.mock_object(
+ self.mock_dest_client, 'enable_volume_dedupe_async')
+ mock_compression_enabled = self.mock_object(
+ self.mock_dest_client, 'enable_volume_compression_async')
+ else:
+ mock_create_flexvol = self.mock_object(self.mock_dest_client,
+ 'create_flexvol')
retval = self.dm_mixin.create_destination_flexvol(
self.src_backend, self.dest_backend,
- self.src_flexvol_name, self.dest_flexvol_name)
+ self.src_flexvol_name, self.dest_flexvol_name,
+ pool_is_flexgroup=pool_is_flexgroup)
self.assertIsNone(retval)
mock_get_provisioning_opts_call.assert_called_once_with(
self.src_flexvol_name)
self.dm_mixin._get_replication_aggregate_map.assert_called_once_with(
self.src_backend, self.dest_backend)
- mock_client_call.assert_called_once_with(
- self.dest_flexvol_name, 'aggr01', fakes.PROVISIONING_OPTS['size'],
- volume_type='dp', **expected_prov_opts)
+
+ if volume_style == 'flexgroup':
+ expected_prov_opts.pop('dedupe_enabled')
+ expected_prov_opts.pop('compression_enabled')
+ mock_create_volume_async.assert_called_once_with(
+ self.dest_flexvol_name,
+ ['aggr01'],
+ fakes.PROVISIONING_OPTS['size'],
+ volume_type='dp', **expected_prov_opts)
+ mock_volume_state.assert_called_once_with(
+ flexvol_name=self.dest_flexvol_name)
+ mock_dedupe_enabled.assert_called_once_with(
+ self.dest_flexvol_name)
+ mock_compression_enabled.assert_called_once_with(
+ self.dest_flexvol_name)
+ else:
+ mock_create_flexvol.assert_called_once_with(
+ self.dest_flexvol_name,
+ 'aggr01',
+ fakes.PROVISIONING_OPTS['size'],
+ volume_type='dp', **expected_prov_opts)
+
mock_is_flexvol_encrypted.assert_called_once_with(
self.src_flexvol_name, self.src_vserver)
def test_create_encrypted_destination_flexvol(self):
aggr_map = {
- fakes.ENCRYPTED_PROVISIONING_OPTS['aggregate']: 'aggr01',
+ fakes.ENCRYPTED_PROVISIONING_OPTS['aggregate'][0]: 'aggr01',
'aggr20': 'aggr02',
}
provisioning_opts = copy.deepcopy(fakes.ENCRYPTED_PROVISIONING_OPTS)
@@ -637,6 +800,33 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
self.mock_src_config)
self.dm_mixin.create_snapmirror.assert_has_calls(expected_calls)
+ def test_ensure_snapmirrors_number_of_tries_exceeded(self):
+ flexvols = ['nvol1']
+ replication_backends = ['fallback1']
+ mock_error_log = self.mock_object(data_motion.LOG, 'error')
+ self.mock_object(self.dm_mixin, 'get_replication_backend_names',
+ return_value=replication_backends)
+ self.mock_object(self.dm_mixin, 'create_snapmirror',
+ side_effect=na_utils.GeometryHasChangedOnDestination)
+
+ self.assertRaises(na_utils.GeometryHasChangedOnDestination,
+ self.dm_mixin.ensure_snapmirrors,
+ self.mock_src_config,
+ self.src_backend,
+ flexvols)
+
+ self.dm_mixin.get_replication_backend_names.assert_called_once_with(
+ self.mock_src_config)
+
+ excepted_call = mock.call(
+ self.src_backend, replication_backends[0],
+ flexvols[0], flexvols[0])
+ self.dm_mixin.create_snapmirror.assert_has_calls([
+ excepted_call, excepted_call, excepted_call
+ ])
+
+ mock_error_log.assert_called()
+
def test_break_snapmirrors(self):
flexvols = ['nvol1', 'nvol2']
replication_backends = ['fallback1', 'fallback2']
diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py
index b566133e840..6b3d8cf65ea 100644
--- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py
+++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode.py
@@ -1068,7 +1068,7 @@ class Client(client_base.Client):
volume_list.append(volume_attributes)
if len(volume_list) != 1:
- msg = _('Could not find unique volume. Volumes foud: %(vol)s.')
+ msg = _('Could not find unique volume. Volumes found: %(vol)s.')
msg_args = {'vol': volume_list}
raise exception.VolumeBackendAPIException(data=msg % msg_args)
@@ -1118,6 +1118,43 @@ class Client(client_base.Client):
return volumes
+ def get_volume_state(self, junction_path=None, name=None):
+ """Returns volume state for a given name or junction path"""
+
+ volume_id_attributes = {}
+ if junction_path:
+ volume_id_attributes['junction-path'] = junction_path
+ if name:
+ volume_id_attributes['name'] = name
+
+ api_args = {
+ 'query': {
+ 'volume-attributes': {
+ 'volume-id-attributes': volume_id_attributes
+ }
+ },
+ 'desired-attributes': {
+ 'volume-attributes': {
+ 'volume-id-attributes': {
+ 'style-extended': None
+ },
+ 'volume-state-attributes': {
+ 'state': None
+ }
+ }
+ }
+ }
+ result = self.send_iter_request('volume-get-iter', api_args)
+ try:
+ volume_attributes = self.get_unique_volume(result)
+ except exception.VolumeBackendAPIException:
+ return None
+
+ volume_state_attributes = volume_attributes.get_child_by_name(
+ 'volume-state-attributes') or netapp_api.NaElement('none')
+ volume_state = volume_state_attributes.get_child_content('state')
+ return volume_state
+
def get_flexvol(self, flexvol_path=None, flexvol_name=None):
"""Get flexvol attributes needed for the storage service catalog."""
@@ -1396,6 +1433,41 @@ class Client(client_base.Client):
qos_min_name = na_utils.qos_min_feature_name(is_nfs, node_name)
return getattr(self.features, qos_min_name, False).__bool__()
+ def create_volume_async(self, name, aggregate_list, size_gb,
+ space_guarantee_type=None, snapshot_policy=None,
+ language=None, snapshot_reserve=None,
+ volume_type='rw'):
+ """Creates a FlexGroup volume asynchronously."""
+
+ api_args = {
+ 'aggr-list': [{'aggr-name': aggr} for aggr in aggregate_list],
+ 'size': size_gb * units.Gi,
+ 'volume-name': name,
+ 'volume-type': volume_type,
+ }
+ if volume_type == 'dp':
+ snapshot_policy = None
+ else:
+ api_args['junction-path'] = '/%s' % name
+ if snapshot_policy is not None:
+ api_args['snapshot-policy'] = snapshot_policy
+ if space_guarantee_type:
+ api_args['space-reserve'] = space_guarantee_type
+ if language is not None:
+ api_args['language-code'] = language
+ if snapshot_reserve is not None:
+ api_args['percentage-snapshot-reserve'] = six.text_type(
+ snapshot_reserve)
+
+ result = self.connection.send_request('volume-create-async', api_args)
+ job_info = {
+ 'status': result.get_child_content('result-status'),
+ 'jobid': result.get_child_content('result-jobid'),
+ 'error-code': result.get_child_content('result-error-code'),
+ 'error-message': result.get_child_content('result-error-message')
+ }
+ return job_info
+
def create_flexvol(self, flexvol_name, aggregate_name, size_gb,
space_guarantee_type=None, snapshot_policy=None,
language=None, dedupe_enabled=False,
@@ -1496,6 +1568,32 @@ class Client(client_base.Client):
}
self.connection.send_request('sis-set-config', api_args)
+ def enable_volume_dedupe_async(self, volume_name):
+ """Enable deduplication on FlexVol/FlexGroup volume asynchronously."""
+ api_args = {'volume-name': volume_name}
+ self.connection.send_request('sis-enable-async', api_args)
+
+ def disable_volume_dedupe_async(self, volume_name):
+ """Disable deduplication on FlexVol/FlexGroup volume asynchronously."""
+ api_args = {'volume-name': volume_name}
+ self.connection.send_request('sis-disable-async', api_args)
+
+ def enable_volume_compression_async(self, volume_name):
+ """Enable compression on FlexVol/FlexGroup volume asynchronously."""
+ api_args = {
+ 'volume-name': volume_name,
+ 'enable-compression': 'true'
+ }
+ self.connection.send_request('sis-set-config-async', api_args)
+
+ def disable_volume_compression_async(self, volume_name):
+ """Disable compression on FlexVol/FlexGroup volume asynchronously."""
+ api_args = {
+ 'volume-name': volume_name,
+ 'enable-compression': 'false'
+ }
+ self.connection.send_request('sis-set-config-async', api_args)
+
@volume_utils.trace_method
def delete_file(self, path_to_file):
"""Delete file at path."""
@@ -2140,6 +2238,7 @@ class Client(client_base.Client):
'destination-vserver': destination_vserver,
'relationship-type': relationship_type,
}
+
if schedule:
api_args['schedule'] = schedule
if policy:
diff --git a/cinder/volume/drivers/netapp/dataontap/utils/data_motion.py b/cinder/volume/drivers/netapp/dataontap/utils/data_motion.py
index 4bdef034616..6b564d3a059 100644
--- a/cinder/volume/drivers/netapp/dataontap/utils/data_motion.py
+++ b/cinder/volume/drivers/netapp/dataontap/utils/data_motion.py
@@ -21,6 +21,7 @@ SnapMirror, and copy-offload as improvements to brute force data transfer.
"""
from oslo_log import log
+from oslo_service import loopingcall
from oslo_utils import excutils
from cinder import exception
@@ -34,6 +35,9 @@ from cinder.volume import volume_utils
LOG = log.getLogger(__name__)
ENTRY_DOES_NOT_EXIST = "(entry doesn't exist)"
+GEOMETRY_HAS_BEEN_CHANGED = (
+ "Geometry of the destination", # This intends to be a Tuple
+ "has been changed since the SnapMirror relationship was created")
QUIESCE_RETRY_INTERVAL = 5
@@ -128,22 +132,33 @@ class DataMotionMixin(object):
If a SnapMirror relationship already exists and is broken off or
quiesced, resume and re-sync the mirror.
"""
+
dest_backend_config = config_utils.get_backend_configuration(
dest_backend_name)
dest_vserver = dest_backend_config.netapp_vserver
- dest_client = config_utils.get_client_for_backend(
- dest_backend_name, vserver_name=dest_vserver)
-
source_backend_config = config_utils.get_backend_configuration(
src_backend_name)
src_vserver = source_backend_config.netapp_vserver
+ dest_client = config_utils.get_client_for_backend(
+ dest_backend_name, vserver_name=dest_vserver)
+ src_client = config_utils.get_client_for_backend(
+ src_backend_name, vserver_name=src_vserver)
+
+ provisioning_options = (
+ src_client.get_provisioning_options_from_flexvol(
+ src_flexvol_name)
+ )
+ pool_is_flexgroup = provisioning_options.get('is_flexgroup', False)
+
# 1. Create destination 'dp' FlexVol if it doesn't exist
if not dest_client.flexvol_exists(dest_flexvol_name):
- self.create_destination_flexvol(src_backend_name,
- dest_backend_name,
- src_flexvol_name,
- dest_flexvol_name)
+ self.create_destination_flexvol(
+ src_backend_name,
+ dest_backend_name,
+ src_flexvol_name,
+ dest_flexvol_name,
+ pool_is_flexgroup=pool_is_flexgroup)
# 2. Check if SnapMirror relationship exists
existing_mirrors = dest_client.get_snapmirrors(
@@ -158,28 +173,48 @@ class DataMotionMixin(object):
# 3. Create and initialize SnapMirror if it doesn't already exist
if not existing_mirrors:
+
# TODO(gouthamr): Change the schedule from hourly to a config value
msg = ("Creating a SnapMirror relationship between "
"%(src_vserver)s:%(src_volume)s and %(dest_vserver)s:"
"%(dest_volume)s.")
LOG.debug(msg, msg_payload)
- dest_client.create_snapmirror(src_vserver,
- src_flexvol_name,
- dest_vserver,
- dest_flexvol_name,
- schedule='hourly')
+ try:
+ dest_client.create_snapmirror(
+ src_vserver,
+ src_flexvol_name,
+ dest_vserver,
+ dest_flexvol_name,
+ schedule='hourly',
+ relationship_type=('extended_data_protection'
+ if pool_is_flexgroup
+ else 'data_protection'))
- msg = ("Initializing SnapMirror transfers between "
- "%(src_vserver)s:%(src_volume)s and %(dest_vserver)s:"
- "%(dest_volume)s.")
- LOG.debug(msg, msg_payload)
+ msg = ("Initializing SnapMirror transfers between "
+ "%(src_vserver)s:%(src_volume)s and %(dest_vserver)s:"
+ "%(dest_volume)s.")
+ LOG.debug(msg, msg_payload)
- # Initialize async transfer of the initial data
- dest_client.initialize_snapmirror(src_vserver,
- src_flexvol_name,
- dest_vserver,
- dest_flexvol_name)
+ # Initialize async transfer of the initial data
+ dest_client.initialize_snapmirror(src_vserver,
+ src_flexvol_name,
+ dest_vserver,
+ dest_flexvol_name)
+ except netapp_api.NaApiError as e:
+ with excutils.save_and_reraise_exception() as raise_ctxt:
+ if (e.code == netapp_api.EAPIERROR and
+ all(substr in e.message for
+ substr in GEOMETRY_HAS_BEEN_CHANGED)):
+ msg = _("Error creating SnapMirror. Geometry has "
+ "changed on destination volume.")
+ LOG.error(msg)
+ self.delete_snapmirror(src_backend_name,
+ dest_backend_name,
+ src_flexvol_name,
+ dest_flexvol_name)
+ raise_ctxt.reraise = False
+ raise na_utils.GeometryHasChangedOnDestination(msg)
# 4. Try to repair SnapMirror if existing
else:
@@ -191,6 +226,7 @@ class DataMotionMixin(object):
"'%(state)s' state. Attempting to repair it.")
msg_payload['state'] = snapmirror.get('mirror-state')
LOG.debug(msg, msg_payload)
+
dest_client.resume_snapmirror(src_vserver,
src_flexvol_name,
dest_vserver,
@@ -399,7 +435,8 @@ class DataMotionMixin(object):
dest_flexvol_name)
def create_destination_flexvol(self, src_backend_name, dest_backend_name,
- src_flexvol_name, dest_flexvol_name):
+ src_flexvol_name, dest_flexvol_name,
+ pool_is_flexgroup=False):
"""Create a SnapMirror mirror target FlexVol for a given source."""
dest_backend_config = config_utils.get_backend_configuration(
dest_backend_name)
@@ -417,11 +454,7 @@ class DataMotionMixin(object):
src_client.get_provisioning_options_from_flexvol(
src_flexvol_name)
)
-
- if provisioning_options.pop('is_flexgroup', False):
- msg = _("Destination volume cannot be created as FlexGroup for "
- "replication, it must already exist there.")
- raise na_utils.NetAppDriverException(msg)
+ provisioning_options.pop('is_flexgroup')
# If the source is encrypted then the destination needs to be
# encrypted too. Using is_flexvol_encrypted because it includes
@@ -441,23 +474,65 @@ class DataMotionMixin(object):
aggregate_map = self._get_replication_aggregate_map(
src_backend_name, dest_backend_name)
- if not aggregate_map.get(source_aggregate):
- msg = _("Unable to find configuration matching the source "
- "aggregate (%s) and the destination aggregate. Option "
- "netapp_replication_aggregate_map may be incorrect.")
- raise na_utils.NetAppDriverException(
- message=msg % source_aggregate)
-
- destination_aggregate = aggregate_map[source_aggregate]
+ destination_aggregate = []
+ for src_aggr in source_aggregate:
+ dst_aggr = aggregate_map.get(src_aggr, None)
+ if dst_aggr:
+ destination_aggregate.append(dst_aggr)
+ else:
+ msg = _("Unable to find configuration matching the source "
+ "aggregate and the destination aggregate. Option "
+ "netapp_replication_aggregate_map may be incorrect.")
+ raise na_utils.NetAppDriverException(message=msg)
# NOTE(gouthamr): The volume is intentionally created as a Data
# Protection volume; junction-path will be added on breaking
# the mirror.
provisioning_options['volume_type'] = 'dp'
- dest_client.create_flexvol(dest_flexvol_name,
- destination_aggregate,
- size,
- **provisioning_options)
+
+ if pool_is_flexgroup:
+ compression_enabled = provisioning_options.pop(
+ 'compression_enabled', False)
+ # cDOT compression requires that deduplication be enabled.
+ dedupe_enabled = provisioning_options.pop(
+ 'dedupe_enabled', False) or compression_enabled
+
+ dest_client.create_volume_async(
+ dest_flexvol_name,
+ destination_aggregate,
+ size,
+ **provisioning_options)
+
+ timeout = self._get_replication_volume_online_timeout()
+
+ def _wait_volume_is_online():
+ volume_state = dest_client.get_volume_state(
+ flexvol_name=dest_flexvol_name)
+ if volume_state and volume_state == 'online':
+ raise loopingcall.LoopingCallDone()
+
+ try:
+ wait_call = loopingcall.FixedIntervalWithTimeoutLoopingCall(
+ _wait_volume_is_online)
+ wait_call.start(interval=5, timeout=timeout).wait()
+
+ if dedupe_enabled:
+ dest_client.enable_volume_dedupe_async(
+ dest_flexvol_name)
+ if compression_enabled:
+ dest_client.enable_volume_compression_async(
+ dest_flexvol_name)
+
+ except loopingcall.LoopingCallTimeOut:
+ msg = _("Timeout waiting destination FlexGroup to to come "
+ "online.")
+ raise na_utils.NetAppDriverException(msg)
+
+ else:
+ dest_client.create_flexvol(dest_flexvol_name,
+ destination_aggregate[0],
+ size,
+ **provisioning_options)
def ensure_snapmirrors(self, config, src_backend_name, src_flexvol_names):
"""Ensure all the SnapMirrors needed for whole-backend replication."""
@@ -467,10 +542,24 @@ class DataMotionMixin(object):
dest_flexvol_name = src_flexvol_name
- self.create_snapmirror(src_backend_name,
- dest_backend_name,
- src_flexvol_name,
- dest_flexvol_name)
+ retry_exceptions = (
+ na_utils.GeometryHasChangedOnDestination,
+ )
+
+ @utils.retry(retry_exceptions,
+ interval=30, retries=6, backoff_rate=1)
+ def _try_create_snapmirror():
+ self.create_snapmirror(src_backend_name,
+ dest_backend_name,
+ src_flexvol_name,
+ dest_flexvol_name)
+ try:
+ _try_create_snapmirror()
+ except na_utils.NetAppDriverException as e:
+ with excutils.save_and_reraise_exception():
+ if isinstance(e, retry_exceptions):
+ LOG.error("Number of tries exceeded "
+ "while trying to create SnapMirror.")
def break_snapmirrors(self, config, src_backend_name, src_flexvol_names,
chosen_target):
@@ -648,3 +737,6 @@ class DataMotionMixin(object):
self.failed_over_backend_name = active_backend_name
return active_backend_name, volume_updates, []
+
+ def _get_replication_volume_online_timeout(self):
+ return self.configuration.netapp_replication_volume_online_timeout
diff --git a/cinder/volume/drivers/netapp/options.py b/cinder/volume/drivers/netapp/options.py
index 17e41064ba3..1e822e4a696 100644
--- a/cinder/volume/drivers/netapp/options.py
+++ b/cinder/volume/drivers/netapp/options.py
@@ -150,9 +150,10 @@ netapp_replication_opts = [
"mapping between source and destination back ends when "
"using whole back end replication. For every "
"source aggregate associated with a cinder pool (NetApp "
- "FlexVol), you would need to specify the destination "
- "aggregate on the replication target device. A "
- "replication target device is configured with the "
+ "FlexVol/FlexGroup), you would need to specify the "
+ "destination aggregate on the replication target "
+ "device. "
+ "A replication target device is configured with the "
"configuration option replication_device. Specify this "
"option as many times as you have replication devices. "
"Each entry takes the standard dict config form: "
@@ -165,7 +166,12 @@ netapp_replication_opts = [
default=3600, # One Hour
help='The maximum time in seconds to wait for existing '
'SnapMirror transfers to complete before aborting '
- 'during a failover.'), ]
+ 'during a failover.'),
+ cfg.IntOpt('netapp_replication_volume_online_timeout',
+ min=60,
+ default=360, # Default to six minutes
+ help='Sets time in seconds to wait for a replication volume '
+ 'create to complete and go online.')]
netapp_support_opts = [
cfg.StrOpt('netapp_api_trace_pattern',
diff --git a/cinder/volume/drivers/netapp/utils.py b/cinder/volume/drivers/netapp/utils.py
index 695a8697c0d..82e465e737d 100644
--- a/cinder/volume/drivers/netapp/utils.py
+++ b/cinder/volume/drivers/netapp/utils.py
@@ -89,6 +89,10 @@ class NetAppDriverException(exception.VolumeDriverException):
message = _("NetApp Cinder Driver exception.")
+class GeometryHasChangedOnDestination(NetAppDriverException):
+ message = _("Geometry has changed on destination volume.")
+
+
def validate_instantiation(**kwargs):
"""Checks if a driver is instantiated other than by the unified driver.