NetApp ONTAP: Add volume migration functions on REST client

This patch contains the implmentation of the functions used in the
operations related to volume migration. The following functions
were migrated from ZAPI to REST API:

  > get_cluster_name
  > get_vserver_peers
  > create_vserver_peer
  > start_lun_move
  > get_lun_move_status
  > start_lun_copy
  > get_lun_copy_status
  > cancel_lun_copy
  > start_file_copy
  > get_file_copy_status

The unit tests related to these functions were also included
in this patch.

Co-authored-by: Luisa Amaral <luisaa@netapp.com>
Co-authored-by: Nahim Alves de Souza <nahimsouza@outlook.com>

Change-Id: I16c305492bbfa3f6f25fd6e70803d47711276c0a
partially-implements: blueprint netapp-ontap-rest-api-client
This commit is contained in:
Fábio Oliveira 2022-04-20 18:20:08 +00:00 committed by Felipe Rodrigues
parent 560ae9d66a
commit 1c3972752f
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