From 1c3972752f5f75b24a2a89b0b57f327a0ad343f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Oliveira?= Date: Wed, 20 Apr 2022 18:20:08 +0000 Subject: [PATCH] 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 Co-authored-by: Nahim Alves de Souza Change-Id: I16c305492bbfa3f6f25fd6e70803d47711276c0a partially-implements: blueprint netapp-ontap-rest-api-client --- .../drivers/netapp/dataontap/client/fakes.py | 64 ++++ .../client/test_client_cmode_rest.py | 312 +++++++++++++++++- .../drivers/netapp/dataontap/block_cmode.py | 45 ++- .../dataontap/client/client_cmode_rest.py | 218 +++++++++++- 4 files changed, 609 insertions(+), 30 deletions(-) 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 c608e49459c..8f6a5ad8c35 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/fakes.py @@ -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 +} diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py index b4358e8b323..1f2c440cfe8 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py @@ -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) diff --git a/cinder/volume/drivers/netapp/dataontap/block_cmode.py b/cinder/volume/drivers/netapp/dataontap/block_cmode.py index 0f6adca4e0f..f12c28826d1 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_cmode.py +++ b/cinder/volume/drivers/netapp/dataontap/block_cmode.py @@ -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 diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py index 3d5f789b387..05fa18494b2 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py @@ -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