Merge "NetApp ONTAP: Add volume migration functions on REST client"

This commit is contained in:
Zuul 2022-09-10 03:51:17 +00:00 committed by Gerrit Code Review
commit 55239a7fc0
4 changed files with 609 additions and 30 deletions

View File

@ -2994,3 +2994,67 @@ JOB_ERROR_REST = {
"start_time": "2022-02-18T20:08:03+00:00",
"end_time": "2022-02-18T20:08:04+00:00",
}
GET_CLUSTER_NAME_RESPONSE_REST = {
"name": CLUSTER_NAME,
"uuid": "fake-cluster-uuid"
}
GET_VSERVER_PEERS_RECORDS_REST = [
{
"_links": {
"self": {
"href": "/api/resourcelink"
}
},
"applications": [
"snapmirror",
"lun_copy"
],
"name": CLUSTER_NAME,
"peer": {
"cluster": {
"_links": {
"self": {
"href": "/api/resourcelink"
}
},
"name": REMOTE_CLUSTER_NAME,
"uuid": "fake-cluster-uuid-2"
},
"svm": {
"_links": {
"self": {
"href": "/api/resourcelink"
}
},
"name": VSERVER_NAME_2,
"uuid": "fake-svm-uuid-2"
}
},
"state": "peered",
"svm": {
"_links": {
"self": {
"href": "/api/resourcelink"
}
},
"name": VSERVER_NAME,
"uuid": "fake-svm-uuid"
},
"uuid": "fake-cluster-uuid"
}
]
GET_VSERVER_PEERS_RESPONSE_REST = {
"_links": {
"next": {
"href": "/api/resourcelink"
},
"self": {
"href": "/api/resourcelink"
}
},
"num_records": 1,
"records": GET_VSERVER_PEERS_RECORDS_REST
}

View File

@ -1734,7 +1734,8 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
result = self.client._get_first_lun_by_path(lun_path)
self.client._get_lun_by_path.assert_called_once_with(lun_path)
self.client._get_lun_by_path.assert_called_once_with(
lun_path, fields=None)
if is_empty:
self.assertTrue(result is None)
else:
@ -2391,6 +2392,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
mock_send_request.assert_called_once_with(
'/storage/luns', 'post', body=expected_body)
@ddt.data(True, False)
def test_destroy_lun(self, force=True):
path = f'/vol/{fake_client.VOLUME_NAME}/{fake_client.FILE_NAME}'
@ -2402,7 +2404,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
self.mock_object(self.client, 'send_request')
self.client.destroy_lun(path)
self.client.destroy_lun(path, force)
self.client.send_request.assert_called_once_with('/storage/luns/',
'delete', query=query)
@ -3380,3 +3382,309 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
self.client.send_request.assert_has_calls([
mock.call('/storage/volumes', 'patch', body=body, query=query)])
def test_get_cluster_name(self):
query = {'fields': 'name'}
self.mock_object(
self.client, 'send_request',
return_value=fake_client.GET_CLUSTER_NAME_RESPONSE_REST)
result = self.client.get_cluster_name()
self.client.send_request.assert_called_once_with(
'/cluster', 'get', query=query, enable_tunneling=False)
self.assertEqual(
fake_client.GET_CLUSTER_NAME_RESPONSE_REST['name'], result)
@ddt.data(
(fake_client.VSERVER_NAME, fake_client.VSERVER_NAME_2),
(fake_client.VSERVER_NAME, None),
(None, fake_client.VSERVER_NAME_2),
(None, None))
@ddt.unpack
def test_get_vserver_peers(self, svm_name, peer_svm_name):
query = {
'fields': 'svm.name,state,peer.svm.name,peer.cluster.name,'
'applications'
}
if peer_svm_name:
query['name'] = peer_svm_name
if svm_name:
query['svm.name'] = svm_name
vserver_info = fake_client.GET_VSERVER_PEERS_RECORDS_REST[0]
expected_result = [{
'vserver': vserver_info['svm']['name'],
'peer-vserver': vserver_info['peer']['svm']['name'],
'peer-state': vserver_info['state'],
'peer-cluster': vserver_info['peer']['cluster']['name'],
'applications': vserver_info['applications'],
}]
self.mock_object(
self.client, 'send_request',
return_value=fake_client.GET_VSERVER_PEERS_RESPONSE_REST)
result = self.client.get_vserver_peers(
vserver_name=svm_name, peer_vserver_name=peer_svm_name)
self.client.send_request.assert_called_once_with(
'/svm/peers', 'get', query=query, enable_tunneling=False)
self.assertEqual(expected_result, result)
def test_get_vserver_peers_empty(self):
vserver_peers_response = copy.deepcopy(
fake_client.GET_VSERVER_PEERS_RESPONSE_REST)
vserver_peers_response['records'] = []
vserver_peers_response['num_records'] = 0
query = {
'fields': 'svm.name,state,peer.svm.name,peer.cluster.name,'
'applications'
}
self.mock_object(
self.client, 'send_request', return_value=vserver_peers_response)
result = self.client.get_vserver_peers()
self.client.send_request.assert_called_once_with(
'/svm/peers', 'get', query=query, enable_tunneling=False)
self.assertEqual([], result)
@ddt.data(['snapmirror', 'lun_copy'], None)
def test_create_vserver_peer(self, applications):
body = {
'svm.name': fake_client.VSERVER_NAME,
'name': fake_client.VSERVER_NAME_2,
'applications': applications if applications else ['snapmirror']
}
self.mock_object(self.client, 'send_request')
self.client.create_vserver_peer(
fake_client.VSERVER_NAME, fake_client.VSERVER_NAME_2,
vserver_peer_application=applications)
self.client.send_request.assert_called_once_with(
'/svm/peers', 'post', body=body, enable_tunneling=False)
@ddt.data(
(fake.VOLUME_NAME, fake.LUN_NAME),
(None, fake.LUN_NAME),
(fake.VOLUME_NAME, None),
(None, None)
)
@ddt.unpack
def test_start_lun_move(self, src_vol, dest_lun):
src_lun = f'src-lun-{fake.LUN_NAME}'
dest_vol = f'dest-vol-{fake.VOLUME_NAME}'
src_path = f'/vol/{src_vol if src_vol else dest_vol}/{src_lun}'
dest_path = f'/vol/{dest_vol}/{dest_lun if dest_lun else src_lun}'
body = {'name': dest_path}
self.mock_object(self.client, '_lun_update_by_path')
result = self.client.start_lun_move(
src_lun, dest_vol, src_ontap_volume=src_vol,
dest_lun_name=dest_lun)
self.client._lun_update_by_path.assert_called_once_with(
src_path, body)
self.assertEqual(dest_path, result)
@ddt.data(fake_client.LUN_GET_MOVEMENT_REST, None)
def test_get_lun_move_status(self, lun_moved):
dest_path = f'/vol/{fake.VOLUME_NAME}/{fake.LUN_NAME}'
move_status = None
if lun_moved:
move_progress = lun_moved['movement']['progress']
move_status = {
'job-status': move_progress['state'],
'last-failure-reason': move_progress['failure']['message']
}
self.mock_object(self.client, '_get_first_lun_by_path',
return_value=lun_moved)
result = self.client.get_lun_move_status(dest_path)
self.client._get_first_lun_by_path.assert_called_once_with(
dest_path, fields='movement.progress')
self.assertEqual(move_status, result)
@ddt.data(
(fake.VOLUME_NAME, fake.LUN_NAME),
(None, fake.LUN_NAME),
(fake.VOLUME_NAME, None),
(None, None)
)
@ddt.unpack
def test_start_lun_copy(self, src_vol, dest_lun):
src_lun = f'src-lun-{fake.LUN_NAME}'
dest_vol = f'dest-vol-{fake.VOLUME_NAME}'
dest_vserver = f'dest-vserver-{fake.VSERVER_NAME}'
src_path = f'/vol/{src_vol if src_vol else dest_vol}/{src_lun}'
dest_path = f'/vol/{dest_vol}/{dest_lun if dest_lun else src_lun}'
body = {
'name': dest_path,
'copy.source.name': src_path,
'svm.name': dest_vserver
}
self.mock_object(self.client, 'send_request')
result = self.client.start_lun_copy(
src_lun, dest_vol, dest_vserver,
src_ontap_volume=src_vol, src_vserver=fake_client.VSERVER_NAME,
dest_lun_name=dest_lun)
self.client.send_request.assert_called_once_with(
'/storage/luns', 'post', body=body, enable_tunneling=False)
self.assertEqual(dest_path, result)
@ddt.data(fake_client.LUN_GET_COPY_REST, None)
def test_get_lun_copy_status(self, lun_copied):
dest_path = f'/vol/{fake.VOLUME_NAME}/{fake.LUN_NAME}'
copy_status = None
if lun_copied:
copy_progress = lun_copied['copy']['source']['progress']
copy_status = {
'job-status': copy_progress['state'],
'last-failure-reason': copy_progress['failure']['message']
}
self.mock_object(self.client, '_get_first_lun_by_path',
return_value=lun_copied)
result = self.client.get_lun_copy_status(dest_path)
self.client._get_first_lun_by_path.assert_called_once_with(
dest_path, fields='copy.source.progress')
self.assertEqual(copy_status, result)
def test_cancel_lun_copy(self):
dest_path = f'/vol/{fake_client.VOLUME_NAME}/{fake_client.FILE_NAME}'
query = {
'name': dest_path,
'svm.name': fake_client.VSERVER_NAME
}
self.mock_object(self.client, 'send_request')
self.client.cancel_lun_copy(dest_path)
self.client.send_request.assert_called_once_with('/storage/luns/',
'delete', query=query)
def test_cancel_lun_copy_exception(self):
dest_path = f'/vol/{fake_client.VOLUME_NAME}/{fake_client.FILE_NAME}'
query = {
'name': dest_path,
'svm.name': fake_client.VSERVER_NAME
}
self.mock_object(self.client, 'send_request',
side_effect=self._mock_api_error())
self.assertRaises(
netapp_utils.NetAppDriverException,
self.client.cancel_lun_copy,
dest_path)
self.client.send_request.assert_called_once_with('/storage/luns/',
'delete', query=query)
# TODO(rfluisa): Add ddt data with None values for optional parameters to
# improve coverage.
def test_start_file_copy(self):
volume = fake_client.VOLUME_ITEM_SIMPLE_RESPONSE_REST
file_name = fake_client.FILE_NAME
dest_ontap_volume = fake_client.VOLUME_NAME
src_ontap_volume = dest_ontap_volume
dest_file_name = file_name
response = {'job': {'uuid': 'fake-uuid'}}
body = {
'files_to_copy': [
{
'source': {
'path': f'{src_ontap_volume}/{file_name}',
'volume': {
'uuid': volume['uuid']
}
},
'destination': {
'path': f'{dest_ontap_volume}/{dest_file_name}',
'volume': {
'uuid': volume['uuid']
}
}
}
]
}
self.mock_object(self.client, '_get_volume_by_args',
return_value=volume)
self.mock_object(self.client, 'send_request',
return_value=response)
result = self.client.start_file_copy(
file_name, dest_ontap_volume, src_ontap_volume=src_ontap_volume,
dest_file_name=dest_file_name)
self.client.send_request.assert_called_once_with(
'/storage/file/copy', 'post', body=body, enable_tunneling=False)
self.assertEqual(response['job']['uuid'], result)
# TODO(rfluisa): Add ddt data with None values for possible api responses
# to improve coverage.
def test_get_file_copy_status(self):
job_uuid = fake_client.FAKE_UUID
query = {}
query['fields'] = '*'
response = {
'state': 'fake-state',
'error': {
'message': 'fake-error-message'
}
}
expected_result = {
'job-status': response['state'],
'last-failure-reason': response['error']['message']
}
self.mock_object(self.client, 'send_request', return_value=response)
result = self.client.get_file_copy_status(job_uuid)
self.client.send_request.assert_called_once_with(
f'/cluster/jobs/{job_uuid}', 'get', query=query,
enable_tunneling=False)
self.assertEqual(expected_result, result)
@ddt.data(('success', 'complete'), ('failure', 'destroyed'))
@ddt.unpack
def test_get_file_copy_status_translate_state(self, from_state, to_state):
job_uuid = fake_client.FAKE_UUID
query = {}
query['fields'] = '*'
response = {
'state': from_state,
'error': {
'message': 'fake-error-message'
}
}
expected_result = {
'job-status': to_state,
'last-failure-reason': response['error']['message']
}
self.mock_object(self.client, 'send_request', return_value=response)
result = self.client.get_file_copy_status(job_uuid)
self.client.send_request.assert_called_once_with(
f'/cluster/jobs/{job_uuid}', 'get', query=query,
enable_tunneling=False)
self.assertEqual(expected_result, result)

View File

@ -616,25 +616,23 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary,
def _move_lun(self, volume, src_ontap_volume, dest_ontap_volume,
dest_lun_name=None):
"""Moves LUN from an ONTAP volume to another."""
job_uuid = self.zapi_client.start_lun_move(
operation_info = self.zapi_client.start_lun_move(
volume.name, dest_ontap_volume, src_ontap_volume=src_ontap_volume,
dest_lun_name=dest_lun_name)
LOG.debug('Start moving LUN %s from %s to %s. '
'Job UUID is %s.', volume.name, src_ontap_volume,
dest_ontap_volume, job_uuid)
LOG.debug('Start moving LUN %s from %s to %s. ',
volume.name, src_ontap_volume,
dest_ontap_volume)
def _wait_lun_move_complete():
move_status = self.zapi_client.get_lun_move_status(job_uuid)
LOG.debug('Waiting for LUN move job %s to complete. '
'Current status is: %s.', job_uuid,
move_status['job-status'])
move_status = self.zapi_client.get_lun_move_status(operation_info)
LOG.debug('Waiting for LUN move to complete. '
'Current status is: %s.', move_status['job-status'])
if not move_status:
status_error_msg = (_("Error moving LUN %s. The "
"corresponding Job UUID % doesn't "
"exist."))
status_error_msg = (_("Error moving LUN %s. The movement"
"status could not be retrieved."))
raise na_utils.NetAppDriverException(
status_error_msg % (volume.id, job_uuid))
status_error_msg % (volume.id))
elif move_status['job-status'] == 'destroyed':
status_error_msg = (_('Error moving LUN %s. %s.'))
raise na_utils.NetAppDriverException(
@ -676,29 +674,27 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary,
dest_ontap_volume, dest_vserver, dest_lun_name=None,
dest_backend_name=None, cancel_on_error=False):
"""Copies LUN from an ONTAP volume to another."""
job_uuid = self.zapi_client.start_lun_copy(
operation_info = self.zapi_client.start_lun_copy(
volume.name, dest_ontap_volume, dest_vserver,
src_ontap_volume=src_ontap_volume, src_vserver=src_vserver,
dest_lun_name=dest_lun_name)
LOG.debug('Start copying LUN %(vol)s from '
'%(src_vserver)s:%(src_ontap_vol)s to '
'%(dest_vserver)s:%(dest_ontap_vol)s. Job UUID is %(job)s.',
'%(dest_vserver)s:%(dest_ontap_vol)s.',
{'vol': volume.name, 'src_vserver': src_vserver,
'src_ontap_vol': src_ontap_volume,
'dest_vserver': dest_vserver,
'dest_ontap_vol': dest_ontap_volume,
'job': job_uuid})
'dest_ontap_vol': dest_ontap_volume})
def _wait_lun_copy_complete():
copy_status = self.zapi_client.get_lun_copy_status(job_uuid)
LOG.debug('Waiting for LUN copy job %s to complete. Current '
'status is: %s.', job_uuid, copy_status['job-status'])
copy_status = self.zapi_client.get_lun_copy_status(operation_info)
LOG.debug('Waiting for LUN copy job to complete. Current '
'status is: %s.', copy_status['job-status'])
if not copy_status:
status_error_msg = (_("Error copying LUN %s. The "
"corresponding Job UUID % doesn't "
"exist."))
status_error_msg = (_("Error copying LUN %s. The copy"
"status could not be retrieved."))
raise na_utils.NetAppDriverException(
status_error_msg % (volume.id, job_uuid))
status_error_msg % (volume.id))
elif copy_status['job-status'] == 'destroyed':
status_error_msg = (_('Error copying LUN %s. %s.'))
raise na_utils.NetAppDriverException(
@ -717,7 +713,8 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary,
except Exception as e:
with excutils.save_and_reraise_exception() as ctxt:
if cancel_on_error:
self._cancel_lun_copy(job_uuid, volume, dest_ontap_volume,
self._cancel_lun_copy(operation_info, volume,
dest_ontap_volume,
dest_backend_name=dest_backend_name)
if isinstance(e, loopingcall.LoopingCallTimeOut):
ctxt.reraise = False

View File

@ -1338,15 +1338,19 @@ class RestClient(object):
self._lun_update_by_path(path, body)
def _get_lun_by_path(self, path):
def _get_lun_by_path(self, path, fields=None):
query = {'name': path}
if fields:
query['fields'] = fields
response = self.send_request('/storage/luns', 'get', query=query)
records = response.get('records', [])
return records
def _get_first_lun_by_path(self, path):
records = self._get_lun_by_path(path)
def _get_first_lun_by_path(self, path, fields=None):
records = self._get_lun_by_path(path, fields=fields)
if len(records) == 0:
return None
@ -2282,8 +2286,214 @@ class RestClient(object):
def mount_flexvol(self, flexvol_name, junction_path=None):
"""Mounts a volume on a junction path."""
query = {'name': flexvol_name}
body = {'nas.path': (
junction_path if junction_path else '/%s' % flexvol_name)}
self.send_request('/storage/volumes', 'patch', query=query, body=body)
def get_cluster_name(self):
"""Gets cluster name."""
query = {'fields': 'name'}
response = self.send_request('/cluster', 'get', query=query,
enable_tunneling=False)
return response['name']
def get_vserver_peers(self, vserver_name=None, peer_vserver_name=None):
"""Gets one or more Vserver peer relationships."""
query = {
'fields': 'svm.name,state,peer.svm.name,peer.cluster.name,'
'applications'
}
if peer_vserver_name:
query['name'] = peer_vserver_name
if vserver_name:
query['svm.name'] = vserver_name
response = self.send_request('/svm/peers', 'get', query=query,
enable_tunneling=False)
records = response.get('records', [])
vserver_peers = []
for vserver_info in records:
vserver_peer = {
'vserver': vserver_info['svm']['name'],
'peer-vserver': vserver_info['peer']['svm']['name'],
'peer-state': vserver_info['state'],
'peer-cluster': vserver_info['peer']['cluster']['name'],
'applications': vserver_info['applications'],
}
vserver_peers.append(vserver_peer)
return vserver_peers
def create_vserver_peer(self, vserver_name, peer_vserver_name,
vserver_peer_application=None):
"""Creates a Vserver peer relationship."""
# default peering application to `snapmirror` if none is specified.
if not vserver_peer_application:
vserver_peer_application = ['snapmirror']
body = {
'svm.name': vserver_name,
'name': peer_vserver_name,
'applications': vserver_peer_application
}
self.send_request('/svm/peers', 'post', body=body,
enable_tunneling=False)
def start_lun_move(self, lun_name, dest_ontap_volume,
src_ontap_volume=None, dest_lun_name=None):
"""Starts a lun move operation between ONTAP volumes."""
if dest_lun_name is None:
dest_lun_name = lun_name
if src_ontap_volume is None:
src_ontap_volume = dest_ontap_volume
src_path = f'/vol/{src_ontap_volume}/{lun_name}'
dest_path = f'/vol/{dest_ontap_volume}/{dest_lun_name}'
body = {'name': dest_path}
self._lun_update_by_path(src_path, body)
return dest_path
def get_lun_move_status(self, dest_path):
"""Get lun move job status from a given dest_path."""
lun = self._get_first_lun_by_path(
dest_path, fields='movement.progress')
if not lun:
return None
move_progress = lun['movement']['progress']
move_status = {
'job-status': move_progress['state'],
'last-failure-reason': (move_progress
.get('failure', {})
.get('message', None))
}
return move_status
def start_lun_copy(self, lun_name, dest_ontap_volume, dest_vserver,
src_ontap_volume=None, src_vserver=None,
dest_lun_name=None):
"""Starts a lun copy operation between ONTAP volumes."""
if src_ontap_volume is None:
src_ontap_volume = dest_ontap_volume
if src_vserver is None:
src_vserver = dest_vserver
if dest_lun_name is None:
dest_lun_name = lun_name
src_path = f'/vol/{src_ontap_volume}/{lun_name}'
dest_path = f'/vol/{dest_ontap_volume}/{dest_lun_name}'
body = {
'name': dest_path,
'copy.source.name': src_path,
'svm.name': dest_vserver
}
self.send_request('/storage/luns', 'post', body=body,
enable_tunneling=False)
return dest_path
def get_lun_copy_status(self, dest_path):
"""Get lun copy job status from a given dest_path."""
lun = self._get_first_lun_by_path(
dest_path, fields='copy.source.progress')
if not lun:
return None
copy_progress = lun['copy']['source']['progress']
copy_status = {
'job-status': copy_progress['state'],
'last-failure-reason': (copy_progress
.get('failure', {})
.get('message', None))
}
return copy_status
def cancel_lun_copy(self, dest_path):
"""Cancel an in-progress lun copy by deleting the lun."""
query = {
'name': dest_path,
'svm.name': self.vserver
}
try:
self.send_request('/storage/luns/', 'delete', query=query)
except netapp_api.NaApiError as e:
msg = (_('Could not cancel lun copy by deleting lun at %s. %s'))
raise na_utils.NetAppDriverException(msg % (dest_path, e))
def start_file_copy(self, file_name, dest_ontap_volume,
src_ontap_volume=None,
dest_file_name=None):
"""Starts a file copy operation between ONTAP volumes."""
if src_ontap_volume is None:
src_ontap_volume = dest_ontap_volume
if dest_file_name is None:
dest_file_name = file_name
source_vol = self._get_volume_by_args(src_ontap_volume)
dest_vol = source_vol
if dest_ontap_volume != src_ontap_volume:
dest_vol = self._get_volume_by_args(dest_ontap_volume)
body = {
'files_to_copy': [
{
'source': {
'path': f'{src_ontap_volume}/{file_name}',
'volume': {
'uuid': source_vol['uuid']
}
},
'destination': {
'path': f'{dest_ontap_volume}/{dest_file_name}',
'volume': {
'uuid': dest_vol['uuid']
}
}
}
]
}
result = self.send_request('/storage/file/copy', 'post', body=body,
enable_tunneling=False)
return result['job']['uuid']
def get_file_copy_status(self, job_uuid):
"""Get file copy job status from a given job's UUID."""
# TODO(rfluisa): Select only the fields that are needed here.
query = {}
query['fields'] = '*'
result = self.send_request(
f'/cluster/jobs/{job_uuid}', 'get', query=query,
enable_tunneling=False)
if not result or not result.get('state', None):
return None
state = result.get('state')
if state == 'success':
state = 'complete'
elif state == 'failure':
state = 'destroyed'
copy_status = {
'job-status': state,
'last-failure-reason': result.get('error', {}).get('message', None)
}
return copy_status