}}"""
# A fake delete file system response of cloudbyte's elasticenter
-FAKE_DELETE_FILE_SYSTEM_RESPONSE = """{ "deleteResponse" : {
- "response" : [{
- "code": "0",
- "description": "success"
- }]
+FAKE_DELETE_FILE_SYSTEM_RESPONSE = """{ "deleteFileSystemResponse" : {
+ "jobid": "e1fe861a-17e3-41b5-ae7c-937caac62cdf"
}}"""
# A fake create storage snapshot response of cloudbyte's elasticenter
self.driver.configuration.cb_tsm_name = 'openstack'
self.driver.configuration.cb_account_name = 'CustomerA'
self.driver.configuration.cb_auth_group = 'fakeauthgroup'
+ self.driver.configuration.cb_apikey = 'G4ZUB39WH7lbiZhPhL3nbd'
+ self.driver.configuration.san_ip = '172.16.51.30'
def _side_effect_api_req(self, cmd, params, version='1.0'):
"""This is a side effect function.
return MAP_COMMAND_TO_FAKE_RESPONSE[cmd]
+ def _side_effect_api_req_to_delete_file_system(
+ self, cmd, params, version='1.0'):
+ """This is a side effect function."""
+ if cmd == 'deleteFileSystem':
+ return {}
+
+ return MAP_COMMAND_TO_FAKE_RESPONSE[cmd]
+
+ def _side_effect_api_req_to_query_asyncjob_response(
+ self, cmd, params, version='1.0'):
+ """This is a side effect function."""
+ if cmd == 'queryAsyncJobResult':
+ return {}
+
+ return MAP_COMMAND_TO_FAKE_RESPONSE[cmd]
+
def _side_effect_api_req_to_query_asyncjob(
self, cmd, params, version='1.0'):
"""This is a side effect function."""
# run the test
self.driver.delete_volume(volume)
- # assert that 2 api calls were invoked
- self.assertEqual(2, mock_api_req.call_count)
+ # assert that 3 api calls were invoked
+ self.assertEqual(3, mock_api_req.call_count)
# Test-II
# assert that no api calls were invoked
self.assertEqual(0, mock_api_req.call_count)
+ # Test-III
+
+ # re-configure the dependencies
+ volume['provider_id'] = fake_volume_id
+
+ # reset & re-configure mock
+ mock_api_req.reset_mock()
+
+ # configure or re-configure the mocks
+ mock_api_req.side_effect = (
+ self._side_effect_api_req_to_delete_file_system)
+
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.delete_volume,
+ volume)
+
+ # assert that 2 api calls were invoked
+ self.assertEqual(2, mock_api_req.call_count)
+
+ # Test - IV
+
+ # reset the mocks
+ mock_api_req.reset_mock()
+
+ # configure or re-configure the mocks
+ mock_api_req.side_effect = (
+ self._side_effect_api_req_to_query_asyncjob_response)
+
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.delete_volume,
+ volume)
+
+ # assert that 3 api calls were invoked
+ self.assertEqual(3, mock_api_req.call_count)
+
@mock.patch.object(cloudbyte.CloudByteISCSIDriver,
'_api_request_for_cloudbyte')
def test_delete_snapshot(self, mock_api_req):
mock_api_req.reset_mock()
mock_api_req.side_effect = self._side_effect_api_req
- # now run the test & assert the exception
- with testtools.ExpectedException(
- exception.VolumeBackendAPIException,
- "Bad or unexpected response from the storage volume "
- "backend API: Volume \[NotExists\] not found in "
- "CloudByte storage."):
- self.driver.create_volume(volume)
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_volume,
+ volume)
# Test - IV
# configure or re-configure the mocks
mock_api_req.side_effect = self._side_effect_api_req_to_create_vol
- # now run the test & assert the exception
- with testtools.ExpectedException(
- exception.VolumeBackendAPIException,
- 'Bad or unexpected response from the storage volume '
- 'backend API: Null response received while '
- 'creating volume'):
- self.driver.create_volume(volume)
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_volume,
+ volume)
# Test - V
# configure or re-configure the mocks
mock_api_req.side_effect = self._side_effect_api_req_to_list_filesystem
- # now run the test
- with testtools.ExpectedException(
- exception.VolumeBackendAPIException,
- "Bad or unexpected response from the storage volume "
- "backend API: Null response received from CloudByte's "
- "list filesystem."):
- self.driver.create_volume(volume)
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_volume,
+ volume)
# Test - VI
mock_api_req.side_effect = (
self._side_effect_api_req_to_list_vol_iscsi_service)
- # now run the test
- with testtools.ExpectedException(
- exception.VolumeBackendAPIException,
- "Bad or unexpected response from the storage volume "
- "backend API: Null response received from CloudByte's "
- "list volume iscsi service."):
- self.driver.create_volume(volume)
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_volume,
+ volume)
# Test - VII
mock_api_req.side_effect = (
self._side_effect_api_req_to_list_iscsi_initiator)
- # now run the test
- with testtools.ExpectedException(
- exception.VolumeBackendAPIException,
- "Bad or unexpected response from the storage volume "
- "backend API: Null response received from CloudByte's "
- "list iscsi initiators."):
- self.driver.create_volume(volume)
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_volume,
+ volume)
# Test - VIII
mock_api_req.side_effect = (
self._none_response_to_list_tsm)
- # now run the test
- with testtools.ExpectedException(
- exception.VolumeBackendAPIException,
- "Bad or unexpected response from the storage volume "
- "backend API: TSM \[openstack\] was not found in CloudByte "
- "storage for account \[CustomerA\]."):
- self.driver.create_volume(volume)
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_volume,
+ volume)
+
+ # Test - IX
+
+ volume['id'] = fake_volume_id
+ volume['size'] = 22
+
+ # reconfigure the dependencies
+ # reset the mocks
+ mock_api_req.reset_mock()
+
+ # configure or re-configure the mocks
+ mock_api_req.side_effect = (
+ self._side_effect_api_req_to_create_vol)
+
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_volume,
+ volume)
+
+ # Test - X
+
+ # reset the mocks
+ mock_api_req.reset_mock()
+
+ # configure or re-configure the mocks
+ mock_api_req.side_effect = (
+ self._side_effect_api_req_to_query_asyncjob_response)
+
+ # Now run the test & assert the exception
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_volume,
+ volume)
@mock.patch.object(cloudbyte.CloudByteISCSIDriver,
'_api_request_for_cloudbyte')
Version history:
1.0.0 - Initial driver
1.1.0 - Add chap support and minor bug fixes
+ 1.1.1 - Add wait logic for delete volumes
"""
- VERSION = '1.1.0'
+ VERSION = '1.1.1'
volume_stats = {}
def __init__(self, *args, **kwargs):
return tsmdetails
+ def _retry_volume_operation(self, operation, retries,
+ max_retries, jobid,
+ cb_volume):
+ """CloudByte async calls via the FixedIntervalLoopingCall."""
+
+ # Query the CloudByte storage with this jobid
+ volume_response = self._queryAsyncJobResult_request(jobid)
+ count = retries['count']
+
+ result_res = None
+ if volume_response is not None:
+ result_res = volume_response.get('queryasyncjobresultresponse')
+
+ if result_res is None:
+ msg = (_(
+ "Null response received while querying "
+ "for [%(operation)s] based job [%(job)s] "
+ "at CloudByte storage.") %
+ {'operation': operation, 'job': jobid})
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ status = result_res.get('jobstatus')
+
+ if status == 1:
+ LOG.info(_LI("CloudByte operation [%(operation)s] succeeded for "
+ "volume [%(cb_volume)s]."),
+ {'operation': operation, 'cb_volume': cb_volume})
+ raise loopingcall.LoopingCallDone()
+ elif count == max_retries:
+ # All attempts exhausted
+ LOG.error(_LE("CloudByte operation [%(operation)s] failed"
+ " for volume [%(vol)s]. Exhausted all"
+ " [%(max)s] attempts."),
+ {'operation': operation,
+ 'vol': cb_volume,
+ 'max': max_retries})
+ raise loopingcall.LoopingCallDone(retvalue=False)
+ else:
+ count += 1
+ retries['count'] = count
+ LOG.debug("CloudByte operation [%(operation)s] for"
+ " volume [%(vol)s]: retry [%(retry)s] of [%(max)s].",
+ {'operation': operation,
+ 'vol': cb_volume,
+ 'retry': count,
+ 'max': max_retries})
+
def _wait_for_volume_creation(self, volume_response, cb_volume_name):
"""Given the job wait for it to complete."""
jobid = vol_res.get('jobid')
if jobid is None:
- msg = _("Jobid not found in CloudByte's "
+ msg = _("Job id not found in CloudByte's "
"create volume [%s] response.") % cb_volume_name
raise exception.VolumeBackendAPIException(data=msg)
- def _retry_check_for_volume_creation():
- """Called at an interval till the volume is created."""
-
- retries = kwargs['retries']
- max_retries = kwargs['max_retries']
- jobid = kwargs['jobid']
- cb_vol = kwargs['cb_vol']
+ retry_interval = (
+ self.configuration.cb_confirm_volume_create_retry_interval)
- # Query the CloudByte storage with this jobid
- volume_response = self._queryAsyncJobResult_request(jobid)
+ max_retries = (
+ self.configuration.cb_confirm_volume_create_retries)
+ retries = {'count': 0}
- result_res = None
- if volume_response is not None:
- result_res = volume_response.get('queryasyncjobresultresponse')
+ timer = loopingcall.FixedIntervalLoopingCall(
+ self._retry_volume_operation,
+ 'Create Volume',
+ retries,
+ max_retries,
+ jobid,
+ cb_volume_name)
+ timer.start(interval=retry_interval).wait()
- if volume_response is None or result_res is None:
- msg = _(
- "Null response received while querying "
- "for create volume job [%s] "
- "at CloudByte storage.") % jobid
- raise exception.VolumeBackendAPIException(data=msg)
+ def _wait_for_volume_deletion(self, volume_response, cb_volume_id):
+ """Given the job wait for it to complete."""
- status = result_res.get('jobstatus')
+ vol_res = volume_response.get('deleteFileSystemResponse')
- if status == 1:
- LOG.info(_LI("Volume [%s] created successfully in "
- "CloudByte storage."), cb_vol)
- raise loopingcall.LoopingCallDone()
+ if vol_res is None:
+ msg = _("Null response received while deleting volume [%s] "
+ "at CloudByte storage.") % cb_volume_id
+ raise exception.VolumeBackendAPIException(data=msg)
- elif retries == max_retries:
- # All attempts exhausted
- LOG.error(_LE("Error in creating volume [%(vol)s] in "
- "CloudByte storage. "
- "Exhausted all [%(max)s] attempts."),
- {'vol': cb_vol, 'max': retries})
- raise loopingcall.LoopingCallDone(retvalue=False)
+ jobid = vol_res.get('jobid')
- else:
- retries += 1
- kwargs['retries'] = retries
- LOG.debug("Wait for volume [%(vol)s] creation, "
- "retry [%(retry)s] of [%(max)s].",
- {'vol': cb_vol,
- 'retry': retries,
- 'max': max_retries})
+ if jobid is None:
+ msg = _("Job id not found in CloudByte's "
+ "delete volume [%s] response.") % cb_volume_id
+ raise exception.VolumeBackendAPIException(data=msg)
retry_interval = (
- self.configuration.cb_confirm_volume_create_retry_interval)
+ self.configuration.cb_confirm_volume_delete_retry_interval)
max_retries = (
- self.configuration.cb_confirm_volume_create_retries)
-
- kwargs = {'retries': 0,
- 'max_retries': max_retries,
- 'jobid': jobid,
- 'cb_vol': cb_volume_name}
+ self.configuration.cb_confirm_volume_delete_retries)
+ retries = {'count': 0}
timer = loopingcall.FixedIntervalLoopingCall(
- _retry_check_for_volume_creation)
+ self._retry_volume_operation,
+ 'Delete Volume',
+ retries,
+ max_retries,
+ jobid,
+ cb_volume_id)
timer.start(interval=retry_interval).wait()
def _get_volume_id_from_response(self, cb_volumes, volume_name):
if cb_volume_id is not None:
params = {"id": cb_volume_id}
- self._api_request_for_cloudbyte('deleteFileSystem', params)
+ del_res = self._api_request_for_cloudbyte('deleteFileSystem',
+ params)
+
+ self._wait_for_volume_deletion(del_res, cb_volume_id)
LOG.info(
_LI("Successfully deleted volume [%(cb_vol)s] "