NetApp ONTAP: Fix FlexGroup replication
Currently driver's implementation use the `volume-create` ZAPI call to create a secondary volume on destination and enable replication. That call only supports the creation of FlexVols, and must be replaced by the async call `volume-create-async` when dealing with FlexGroups. Change-Id: I07fe2e62a046f7276b6dcd78fd25c89412045d70
This commit is contained in:
parent
4fcc583813
commit
1fba4a9e93
|
@ -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 = """
|
||||
<volume-attributes>
|
||||
<volume-id-attributes>
|
||||
<style-extended>flexgroup</style-extended>
|
||||
</volume-id-attributes>
|
||||
<volume-state-attributes>
|
||||
<state>%(state)s</state>
|
||||
</volume-state-attributes>
|
||||
</volume-attributes>
|
||||
""" % {
|
||||
'state': VOLUME_STATE_ONLINE
|
||||
}
|
||||
|
||||
VOLUME_GET_ITER_STATE_ATTR = etree.XML(VOLUME_GET_ITER_STATE_ATTR_STR)
|
||||
|
||||
VOLUME_GET_ITER_STATE_RESPONSE = etree.XML("""
|
||||
<results status="passed">
|
||||
<num-records>1</num-records>
|
||||
<attributes-list> %(volume)s </attributes-list>
|
||||
</results>
|
||||
""" % {
|
||||
'volume': VOLUME_GET_ITER_STATE_ATTR_STR,
|
||||
})
|
||||
|
||||
VOLUME_SIZE_TOTAL = 19922944
|
||||
VOLUME_SIZE_AVAILABLE = 19791872
|
||||
VOLUME_GET_ITER_CAPACITY_ATTR_STR = """
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,7 +324,8 @@ 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(
|
||||
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)
|
||||
|
@ -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'],
|
||||
|
||||
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']
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
self.create_destination_flexvol(
|
||||
src_backend_name,
|
||||
dest_backend_name,
|
||||
src_flexvol_name,
|
||||
dest_flexvol_name)
|
||||
dest_flexvol_name,
|
||||
pool_is_flexgroup=pool_is_flexgroup)
|
||||
|
||||
# 2. Check if SnapMirror relationship exists
|
||||
existing_mirrors = dest_client.get_snapmirrors(
|
||||
|
@ -158,17 +173,23 @@ 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,
|
||||
try:
|
||||
dest_client.create_snapmirror(
|
||||
src_vserver,
|
||||
src_flexvol_name,
|
||||
dest_vserver,
|
||||
dest_flexvol_name,
|
||||
schedule='hourly')
|
||||
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:"
|
||||
|
@ -180,6 +201,20 @@ class DataMotionMixin(object):
|
|||
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,24 +474,66 @@ class DataMotionMixin(object):
|
|||
aggregate_map = self._get_replication_aggregate_map(
|
||||
src_backend_name, dest_backend_name)
|
||||
|
||||
if not aggregate_map.get(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 (%s) and the destination aggregate. Option "
|
||||
"aggregate 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]
|
||||
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,
|
||||
|
||||
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."""
|
||||
backend_names = self.get_replication_backend_names(config)
|
||||
|
@ -467,10 +542,24 @@ class DataMotionMixin(object):
|
|||
|
||||
dest_flexvol_name = src_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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Reference in New Issue