Merge "NetApp ONTAP: Add volume migration functions on REST client"
This commit is contained in:
@@ -2994,3 +2994,67 @@ JOB_ERROR_REST = {
|
|||||||
"start_time": "2022-02-18T20:08:03+00:00",
|
"start_time": "2022-02-18T20:08:03+00:00",
|
||||||
"end_time": "2022-02-18T20:08:04+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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1734,7 +1734,8 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
|
|||||||
|
|
||||||
result = self.client._get_first_lun_by_path(lun_path)
|
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:
|
if is_empty:
|
||||||
self.assertTrue(result is None)
|
self.assertTrue(result is None)
|
||||||
else:
|
else:
|
||||||
@@ -2391,6 +2392,7 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
|
|||||||
mock_send_request.assert_called_once_with(
|
mock_send_request.assert_called_once_with(
|
||||||
'/storage/luns', 'post', body=expected_body)
|
'/storage/luns', 'post', body=expected_body)
|
||||||
|
|
||||||
|
@ddt.data(True, False)
|
||||||
def test_destroy_lun(self, force=True):
|
def test_destroy_lun(self, force=True):
|
||||||
path = f'/vol/{fake_client.VOLUME_NAME}/{fake_client.FILE_NAME}'
|
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.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/',
|
self.client.send_request.assert_called_once_with('/storage/luns/',
|
||||||
'delete', query=query)
|
'delete', query=query)
|
||||||
@@ -3380,3 +3382,309 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
|
|||||||
|
|
||||||
self.client.send_request.assert_has_calls([
|
self.client.send_request.assert_has_calls([
|
||||||
mock.call('/storage/volumes', 'patch', body=body, query=query)])
|
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)
|
||||||
|
|||||||
@@ -616,25 +616,23 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary,
|
|||||||
def _move_lun(self, volume, src_ontap_volume, dest_ontap_volume,
|
def _move_lun(self, volume, src_ontap_volume, dest_ontap_volume,
|
||||||
dest_lun_name=None):
|
dest_lun_name=None):
|
||||||
"""Moves LUN from an ONTAP volume to another."""
|
"""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,
|
volume.name, dest_ontap_volume, src_ontap_volume=src_ontap_volume,
|
||||||
dest_lun_name=dest_lun_name)
|
dest_lun_name=dest_lun_name)
|
||||||
LOG.debug('Start moving LUN %s from %s to %s. '
|
LOG.debug('Start moving LUN %s from %s to %s. ',
|
||||||
'Job UUID is %s.', volume.name, src_ontap_volume,
|
volume.name, src_ontap_volume,
|
||||||
dest_ontap_volume, job_uuid)
|
dest_ontap_volume)
|
||||||
|
|
||||||
def _wait_lun_move_complete():
|
def _wait_lun_move_complete():
|
||||||
move_status = self.zapi_client.get_lun_move_status(job_uuid)
|
move_status = self.zapi_client.get_lun_move_status(operation_info)
|
||||||
LOG.debug('Waiting for LUN move job %s to complete. '
|
LOG.debug('Waiting for LUN move to complete. '
|
||||||
'Current status is: %s.', job_uuid,
|
'Current status is: %s.', move_status['job-status'])
|
||||||
move_status['job-status'])
|
|
||||||
|
|
||||||
if not move_status:
|
if not move_status:
|
||||||
status_error_msg = (_("Error moving LUN %s. The "
|
status_error_msg = (_("Error moving LUN %s. The movement"
|
||||||
"corresponding Job UUID % doesn't "
|
"status could not be retrieved."))
|
||||||
"exist."))
|
|
||||||
raise na_utils.NetAppDriverException(
|
raise na_utils.NetAppDriverException(
|
||||||
status_error_msg % (volume.id, job_uuid))
|
status_error_msg % (volume.id))
|
||||||
elif move_status['job-status'] == 'destroyed':
|
elif move_status['job-status'] == 'destroyed':
|
||||||
status_error_msg = (_('Error moving LUN %s. %s.'))
|
status_error_msg = (_('Error moving LUN %s. %s.'))
|
||||||
raise na_utils.NetAppDriverException(
|
raise na_utils.NetAppDriverException(
|
||||||
@@ -676,29 +674,27 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary,
|
|||||||
dest_ontap_volume, dest_vserver, dest_lun_name=None,
|
dest_ontap_volume, dest_vserver, dest_lun_name=None,
|
||||||
dest_backend_name=None, cancel_on_error=False):
|
dest_backend_name=None, cancel_on_error=False):
|
||||||
"""Copies LUN from an ONTAP volume to another."""
|
"""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,
|
volume.name, dest_ontap_volume, dest_vserver,
|
||||||
src_ontap_volume=src_ontap_volume, src_vserver=src_vserver,
|
src_ontap_volume=src_ontap_volume, src_vserver=src_vserver,
|
||||||
dest_lun_name=dest_lun_name)
|
dest_lun_name=dest_lun_name)
|
||||||
LOG.debug('Start copying LUN %(vol)s from '
|
LOG.debug('Start copying LUN %(vol)s from '
|
||||||
'%(src_vserver)s:%(src_ontap_vol)s to '
|
'%(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,
|
{'vol': volume.name, 'src_vserver': src_vserver,
|
||||||
'src_ontap_vol': src_ontap_volume,
|
'src_ontap_vol': src_ontap_volume,
|
||||||
'dest_vserver': dest_vserver,
|
'dest_vserver': dest_vserver,
|
||||||
'dest_ontap_vol': dest_ontap_volume,
|
'dest_ontap_vol': dest_ontap_volume})
|
||||||
'job': job_uuid})
|
|
||||||
|
|
||||||
def _wait_lun_copy_complete():
|
def _wait_lun_copy_complete():
|
||||||
copy_status = self.zapi_client.get_lun_copy_status(job_uuid)
|
copy_status = self.zapi_client.get_lun_copy_status(operation_info)
|
||||||
LOG.debug('Waiting for LUN copy job %s to complete. Current '
|
LOG.debug('Waiting for LUN copy job to complete. Current '
|
||||||
'status is: %s.', job_uuid, copy_status['job-status'])
|
'status is: %s.', copy_status['job-status'])
|
||||||
if not copy_status:
|
if not copy_status:
|
||||||
status_error_msg = (_("Error copying LUN %s. The "
|
status_error_msg = (_("Error copying LUN %s. The copy"
|
||||||
"corresponding Job UUID % doesn't "
|
"status could not be retrieved."))
|
||||||
"exist."))
|
|
||||||
raise na_utils.NetAppDriverException(
|
raise na_utils.NetAppDriverException(
|
||||||
status_error_msg % (volume.id, job_uuid))
|
status_error_msg % (volume.id))
|
||||||
elif copy_status['job-status'] == 'destroyed':
|
elif copy_status['job-status'] == 'destroyed':
|
||||||
status_error_msg = (_('Error copying LUN %s. %s.'))
|
status_error_msg = (_('Error copying LUN %s. %s.'))
|
||||||
raise na_utils.NetAppDriverException(
|
raise na_utils.NetAppDriverException(
|
||||||
@@ -717,7 +713,8 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary,
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
with excutils.save_and_reraise_exception() as ctxt:
|
with excutils.save_and_reraise_exception() as ctxt:
|
||||||
if cancel_on_error:
|
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)
|
dest_backend_name=dest_backend_name)
|
||||||
if isinstance(e, loopingcall.LoopingCallTimeOut):
|
if isinstance(e, loopingcall.LoopingCallTimeOut):
|
||||||
ctxt.reraise = False
|
ctxt.reraise = False
|
||||||
|
|||||||
@@ -1338,15 +1338,19 @@ class RestClient(object):
|
|||||||
|
|
||||||
self._lun_update_by_path(path, body)
|
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}
|
query = {'name': path}
|
||||||
|
|
||||||
|
if fields:
|
||||||
|
query['fields'] = fields
|
||||||
|
|
||||||
response = self.send_request('/storage/luns', 'get', query=query)
|
response = self.send_request('/storage/luns', 'get', query=query)
|
||||||
records = response.get('records', [])
|
records = response.get('records', [])
|
||||||
|
|
||||||
return records
|
return records
|
||||||
|
|
||||||
def _get_first_lun_by_path(self, path):
|
def _get_first_lun_by_path(self, path, fields=None):
|
||||||
records = self._get_lun_by_path(path)
|
records = self._get_lun_by_path(path, fields=fields)
|
||||||
if len(records) == 0:
|
if len(records) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -2282,8 +2286,214 @@ class RestClient(object):
|
|||||||
|
|
||||||
def mount_flexvol(self, flexvol_name, junction_path=None):
|
def mount_flexvol(self, flexvol_name, junction_path=None):
|
||||||
"""Mounts a volume on a junction path."""
|
"""Mounts a volume on a junction path."""
|
||||||
|
|
||||||
query = {'name': flexvol_name}
|
query = {'name': flexvol_name}
|
||||||
body = {'nas.path': (
|
body = {'nas.path': (
|
||||||
junction_path if junction_path else '/%s' % flexvol_name)}
|
junction_path if junction_path else '/%s' % flexvol_name)}
|
||||||
self.send_request('/storage/volumes', 'patch', query=query, body=body)
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user