}]
}}"""
+# A fake list iSCSI auth user response of cloudbyte's elasticenter
+FAKE_LIST_ISCSI_AUTH_USER_RESPONSE = """{ "listiSCSIAuthUsersResponse" : {
+ "count":1 ,
+ "authuser" : [{
+ "id": "53d00164-a974-31b8-a854-bd346a8ea937",
+ "accountid": "12d41531-c41a-4ab7-abe2-ce0db2570119",
+ "authgroupid": "537744eb-c594-3145-85c0-96079922b894",
+ "chapusername": "fakeauthgroupchapuser",
+ "chappassword": "fakeauthgroupchapsecret",
+ "mutualchapusername": "fakeauthgroupmutualchapuser",
+ "mutualchappassword": "fakeauthgroupmutualchapsecret"
+ }]
+ }}"""
+
+# A fake list iSCSI auth group response of cloudbyte's elasticenter
+FAKE_LIST_ISCSI_AUTH_GROUP_RESPONSE = """{ "listiSCSIAuthGroupResponse" : {
+ "count":2 ,
+ "authgroup" : [{
+ "id": "32d935ee-a60f-3681-b792-d8ccfe7e8e7f",
+ "name": "None",
+ "comment": "None"
+ }, {
+ "id": "537744eb-c594-3145-85c0-96079922b894",
+ "name": "fakeauthgroup",
+ "comment": "Fake Auth Group For Openstack "
+ }]
+ }}"""
+
# This dict maps the http commands of elasticenter
# with its respective fake responses
json.loads(FAKE_LIST_FILE_SYSTEM_RESPONSE))
MAP_COMMAND_TO_FAKE_RESPONSE["deleteSnapshot"] = (
json.loads(FAKE_DELETE_STORAGE_SNAPSHOT_RESPONSE))
-MAP_COMMAND_TO_FAKE_RESPONSE["listStorageSnapshots"] = (
- json.loads(FAKE_LIST_STORAGE_SNAPSHOTS_RESPONSE))
MAP_COMMAND_TO_FAKE_RESPONSE["updateVolumeiSCSIService"] = (
json.loads(FAKE_UPDATE_VOLUME_ISCSI_SERVICE_RESPONSE))
MAP_COMMAND_TO_FAKE_RESPONSE["createStorageSnapshot"] = (
json.loads(FAKE_UPDATE_QOS_GROUP_RESPONSE))
MAP_COMMAND_TO_FAKE_RESPONSE['listStorageSnapshots'] = (
json.loads(FAKE_LIST_STORAGE_SNAPSHOTS_RESPONSE))
-
-# This dict maps the http commands of elasticenter
-# with its respective fake json responses
-MAP_COMMAND_TO_FAKE_JSON_RESPONSE = {}
-
-MAP_COMMAND_TO_FAKE_JSON_RESPONSE["listTsm"] = FAKE_LIST_TSM_RESPONSE
+MAP_COMMAND_TO_FAKE_RESPONSE['listiSCSIAuthUser'] = (
+ json.loads(FAKE_LIST_ISCSI_AUTH_USER_RESPONSE))
+MAP_COMMAND_TO_FAKE_RESPONSE['listiSCSIAuthGroup'] = (
+ json.loads(FAKE_LIST_ISCSI_AUTH_GROUP_RESPONSE))
class CloudByteISCSIDriverTestCase(testtools.TestCase):
# override some parts of driver configuration
self.driver.configuration.cb_tsm_name = 'openstack'
self.driver.configuration.cb_account_name = 'CustomerA'
+ self.driver.configuration.cb_auth_group = 'fakeauthgroup'
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_list_iscsi_auth_group(self, cmd, params,
+ version='1.0'):
+ """This is a side effect function."""
+ if cmd == 'listiSCSIAuthGroup':
+ return {}
+
+ return MAP_COMMAND_TO_FAKE_RESPONSE[cmd]
+
+ def _side_effect_api_req_to_list_iscsi_auth_user(self, cmd, params,
+ version='1.0'):
+ """This is a side effect function."""
+ if cmd == 'listiSCSIAuthUser':
+ return {}
+
+ return MAP_COMMAND_TO_FAKE_RESPONSE[cmd]
+
+ def _side_effect_enable_chap(self):
+ """This is a side effect function."""
+ self.driver.cb_use_chap = True
+
+ def _side_effect_disable_chap(self):
+ """This is a side effect function."""
+ self.driver.cb_use_chap = False
+
def _side_effect_api_req_to_list_filesystem(
self, cmd, params, version='1.0'):
"""This is a side effect function."""
# Test - I
+ # enable CHAP
+ self._side_effect_enable_chap()
+
# configure the mocks with respective side-effects
mock_api_req.side_effect = self._side_effect_api_req
'openstack', self.driver.configuration.cb_tsm_name)
self.assertEqual(
'CustomerA', self.driver.configuration.cb_account_name)
+ self.assertEqual(
+ 'fakeauthgroup', self.driver.configuration.cb_auth_group)
+
+ # assert the result
+ self.assertEqual(
+ 'CHAP fakeauthgroupchapuser fakeauthgroupchapsecret',
+ provider_details['provider_auth'])
self.assertThat(
provider_details['provider_location'],
matchers.Contains('172.16.50.35:3260'))
- # assert that 9 api calls were invoked
- self.assertEqual(9, mock_api_req.call_count)
+ # assert the invoked api calls to CloudByte Storage
+ self.assertEqual(11, mock_api_req.call_count)
# Test - II
+ # reset the mock
+ mock_api_req.reset_mock()
+
+ # disable CHAP
+ self._side_effect_disable_chap()
+
+ # configure the mocks with respective side-effects
+ mock_api_req.side_effect = self._side_effect_api_req
+
+ # now run the test
+ provider_details = self.driver.create_volume(volume)
+
+ # assert equality checks for certain configuration attributes
+ self.assertEqual(
+ 'openstack', self.driver.configuration.cb_tsm_name)
+ self.assertEqual(
+ 'CustomerA', self.driver.configuration.cb_account_name)
+
+ # assert the result
+ self.assertEqual(
+ None,
+ provider_details['provider_auth'])
+ self.assertThat(
+ provider_details['provider_location'],
+ matchers.Contains('172.16.50.35:3260'))
+
+ # assert the invoked api calls to CloudByte Storage
+ self.assertEqual(9, mock_api_req.call_count)
+
+ # Test - III
+
# reconfigure the dependencies
volume['id'] = 'NotExists'
del volume['size']
"CloudByte storage."):
self.driver.create_volume(volume)
- # Test - III
+ # Test - IV
# reconfigure the dependencies
volume['id'] = 'abc'
'creating volume'):
self.driver.create_volume(volume)
- # Test - IV
+ # Test - V
# reconfigure the dependencies
# reset the mocks
# Test - I
+ # enable CHAP
+ self._side_effect_enable_chap()
+
+ # configure the mocks with respective side-effects
+ mock_api_req.side_effect = self._side_effect_api_req
+
+ # now run the test
+ provider_details = (
+ self.driver.create_volume_from_snapshot(cloned_volume, snapshot))
+
+ # assert the result
+ self.assertEqual(
+ 'CHAP fakeauthgroupchapuser fakeauthgroupchapsecret',
+ provider_details['provider_auth'])
+ self.assertEqual(
+ '20.10.22.56:3260 '
+ 'iqn.2014-06.acc1.openstacktsm:acc1DS1Snap1clone1 0',
+ provider_details['provider_location'])
+
+ # assert the invoked api calls to CloudByte Storage
+ self.assertEqual(4, mock_api_req.call_count)
+
+ # Test - II
+
+ # reset the mocks
+ mock_api_req.reset_mock()
+
+ # disable CHAP
+ self._side_effect_disable_chap()
+
# configure the mocks with respective side-effects
mock_api_req.side_effect = self._side_effect_api_req
# now run the test
- self.driver.create_volume_from_snapshot(cloned_volume, snapshot)
+ provider_details = (
+ self.driver.create_volume_from_snapshot(cloned_volume, snapshot))
+
+ # assert the result
+ self.assertEqual(
+ None,
+ provider_details['provider_auth'])
+ self.assertEqual(
+ '20.10.22.56:3260 '
+ 'iqn.2014-06.acc1.openstacktsm:acc1DS1Snap1clone1 0',
+ provider_details['provider_location'])
# assert n api calls were invoked
self.assertEqual(1, mock_api_req.call_count)
'_api_request_for_cloudbyte')
def test_create_export(self, mock_api_req):
- # prepare the input test data
+ # Test - I
+
+ # enable CHAP
+ self._side_effect_enable_chap()
# configure the mocks with respective side-effects
mock_api_req.side_effect = self._side_effect_api_req
model_update = self.driver.create_export({}, {})
# assert the result
- self.assertEqual(None, model_update['provider_auth'])
+ self.assertEqual('CHAP fakeauthgroupchapuser fakeauthgroupchapsecret',
+ model_update['provider_auth'])
+
+ # Test - II
+
+ # reset the mocks
+ mock_api_req.reset_mock()
+
+ # disable CHAP
+ self._side_effect_disable_chap()
+
+ # configure the mocks with respective side-effects
+ mock_api_req.side_effect = self._side_effect_api_req
+
+ # now run the test
+ model_update = self.driver.create_export({}, {})
+
+ # assert the result
+ self.assertEqual(None,
+ model_update['provider_auth'])
@mock.patch.object(cloudbyte.CloudByteISCSIDriver,
'_api_request_for_cloudbyte')
def test_ensure_export(self, mock_api_req):
- # prepare the input test data
+ # Test - I
+
+ # enable CHAP
+ self._side_effect_enable_chap()
# configure the mock with respective side-effects
mock_api_req.side_effect = self._side_effect_api_req
model_update = self.driver.ensure_export({}, {})
# assert the result to have a provider_auth attribute
- self.assertEqual(None, model_update['provider_auth'])
+ self.assertEqual('CHAP fakeauthgroupchapuser fakeauthgroupchapsecret',
+ model_update['provider_auth'])
+
+ # Test - II
+
+ # reset the mocks
+ mock_api_req.reset_mock()
+
+ # disable CHAP
+ self._side_effect_disable_chap()
+
+ # configure the mocks with respective side-effects
+ mock_api_req.side_effect = self._side_effect_api_req
+
+ # now run the test
+ model_update = self.driver.create_export({}, {})
+
+ # assert the result
+ self.assertEqual(None,
+ model_update['provider_auth'])
@mock.patch.object(cloudbyte.CloudByteISCSIDriver,
'_api_request_for_cloudbyte')
def test_get_volume_stats(self, mock_api_req):
- # prepare the input test data
-
# configure the mock with a side-effect
mock_api_req.side_effect = self._side_effect_api_req
Version history:
1.0.0 - Initial driver
+ 1.1.0 - Add chap support and minor bug fixes
"""
- VERSION = '1.0.0'
+ VERSION = '1.1.0'
volume_stats = {}
def __init__(self, *args, **kwargs):
options.cloudbyte_create_volume_opts)
self.configuration.append_config_values(
options.cloudbyte_connection_opts)
+ self.cb_use_chap = self.configuration.use_chap_auth
self.get_volume_stats()
def _get_url(self, cmd, params, apikey):
return qosgroup_id
- def _build_provider_details_from_volume(self, volume):
+ def _build_provider_details_from_volume(self, volume, chap):
model_update = {}
model_update['provider_location'] = (
# Will provide CHAP Authentication on forthcoming patches/release
model_update['provider_auth'] = None
+ if chap:
+ model_update['provider_auth'] = ('CHAP %(username)s %(password)s'
+ % chap)
+
model_update['provider_id'] = volume['id']
- LOG.debug("CloudByte volume [%(vol)s] properties: [%(props)s].",
- {'vol': volume['iqnname'], 'props': model_update})
+ LOG.debug("CloudByte volume iqn: [%(iqn)s] provider id: [%(proid)s].",
+ {'iqn': volume['iqnname'], 'proid': volume['id']})
return model_update
- def _build_provider_details_from_response(self, cb_volumes, volume_name):
+ def _build_provider_details_from_response(self,
+ cb_volumes,
+ volume_name,
+ chap):
"""Get provider information."""
model_update = {}
for vol in volumes:
if vol['name'] == volume_name:
- model_update = self._build_provider_details_from_volume(vol)
+ model_update = self._build_provider_details_from_volume(vol,
+ chap)
break
return model_update
else:
return iscsi_id
- def _request_update_iscsi_service(self, iscsi_id, ig_id):
+ def _request_update_iscsi_service(self, iscsi_id, ig_id, ag_id):
params = {
"id": iscsi_id,
"igid": ig_id
}
+ if ag_id:
+ params['authgroupid'] = ag_id
+ params['authmethod'] = "CHAP"
+
self._api_request_for_cloudbyte(
'updateVolumeiSCSIService', params)
return data
+ def _get_auth_group_id_from_response(self, data):
+ """Find iSCSI auth group id."""
+
+ chap_group = self.configuration.cb_auth_group
+
+ ag_list_res = data.get('listiSCSIAuthGroupResponse')
+
+ if ag_list_res is None:
+ msg = _("Null response received from CloudByte's "
+ "list iscsi auth groups.")
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ ag_list = ag_list_res.get('authgroup')
+
+ if ag_list is None:
+ msg = _('No iscsi auth groups were found in CloudByte.')
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ ag_id = None
+
+ for ag in ag_list:
+ if ag.get('name') == chap_group:
+ ag_id = ag['id']
+ break
+ else:
+ msg = _("Auth group [%s] details not found in "
+ "CloudByte storage.") % chap_group
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ return ag_id
+
+ def _get_auth_group_info(self, account_id, ag_id):
+ """Fetch the auth group details."""
+
+ params = {"accountid": account_id, "authgroupid": ag_id}
+
+ auth_users = self._api_request_for_cloudbyte(
+ 'listiSCSIAuthUser', params)
+
+ auth_user_details_res = auth_users.get('listiSCSIAuthUsersResponse')
+
+ if auth_user_details_res is None:
+ msg = _("No response was received from CloudByte storage "
+ "list iSCSI auth user API call.")
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ auth_user_details = auth_user_details_res.get('authuser')
+
+ if auth_user_details is None:
+ msg = _("Auth user details not found in CloudByte storage.")
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ chapuser = auth_user_details[0].get('chapusername')
+ chappassword = auth_user_details[0].get('chappassword')
+
+ if chapuser is None or chappassword is None:
+ msg = _("Invalid chap user details found in CloudByte storage.")
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ data = {'username': chapuser, 'password': chappassword, 'ag_id': ag_id}
+
+ return data
+
+ def _get_chap_info(self, account_id):
+ """Fetch the chap details."""
+
+ params = {"accountid": account_id}
+
+ iscsi_auth_data = self._api_request_for_cloudbyte(
+ 'listiSCSIAuthGroup', params)
+
+ ag_id = self._get_auth_group_id_from_response(
+ iscsi_auth_data)
+
+ return self._get_auth_group_info(account_id, ag_id)
+
+ def _export(self):
+ model_update = {'provider_auth': None}
+
+ if self.cb_use_chap is True:
+ account_name = self.configuration.cb_account_name
+
+ account_id = self._get_account_id_from_name(account_name)
+
+ chap = self._get_chap_info(account_id)
+
+ model_update['provider_auth'] = ('CHAP %(username)s %(password)s'
+ % chap)
+
+ return model_update
+
def create_volume(self, volume):
tsm_name = self.configuration.cb_tsm_name
LOG.debug("Updating iscsi service for CloudByte volume [%s].",
cb_volume_name)
+ ag_id = None
+ chap_info = {}
+
+ if self.cb_use_chap is True:
+ chap_info = self._get_chap_info(account_id)
+ ag_id = chap_info['ag_id']
+
# Update the iscsi service with above fetched iscsi_id & ig_id
- self._request_update_iscsi_service(iscsi_id, ig_id)
+ self._request_update_iscsi_service(iscsi_id, ig_id, ag_id)
LOG.debug("CloudByte volume [%(vol)s] updated with "
- "iscsi id [%(iscsi)s] and ig id [%(ig)s].",
- {'vol': cb_volume_name, 'iscsi': iscsi_id, 'ig': ig_id})
+ "iscsi id [%(iscsi)s] and initiator group [%(ig)s] and "
+ "authentication group [%(ag)s].",
+ {'vol': cb_volume_name, 'iscsi': iscsi_id,
+ 'ig': ig_id, 'ag': ag_id})
# Provide the model after successful completion of above steps
provider = self._build_provider_details_from_response(
- cb_volumes, cb_volume_name)
+ cb_volumes, cb_volume_name, chap_info)
LOG.info(_LI("Successfully created a CloudByte volume [%(cb_vol)s] "
"w.r.t OpenStack volume [%(stack_vol)s]."),
'cb_snap': cb_snapshot_path,
'stack_vol': parent_volume_id})
- return self._build_provider_details_from_volume(cb_vol)
+ chap_info = {}
+
+ if self.cb_use_chap is True:
+ account_name = self.configuration.cb_account_name
+
+ # Get account id of this account
+ account_id = self._get_account_id_from_name(account_name)
+
+ chap_info = self._get_chap_info(account_id)
+
+ model_update = self._build_provider_details_from_volume(cb_vol,
+ chap_info)
+
+ return model_update
def delete_snapshot(self, snapshot):
"""Delete a snapshot at CloudByte."""
def create_export(self, context, volume):
"""Setup the iscsi export info."""
- model_update = {}
-
- # Will provide CHAP Authentication on forthcoming patches/release
- model_update['provider_auth'] = None
- return model_update
+ return self._export()
def ensure_export(self, context, volume):
"""Verify the iscsi export info."""
- model_update = {}
- # Will provide CHAP Authentication on forthcoming patches/release
- model_update['provider_auth'] = None
-
- return model_update
+ return self._export()
def get_volume_stats(self, refresh=False):
"""Get volume statistics.