self.assertRaises(exception.SolidFireAPIException,
sfv._get_sfaccount_by_name, 'some-name')
- @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request')
- @mock.patch.object(solidfire.SolidFireDriver, '_create_template_account')
- def test_delete_volume(self,
- _mock_create_template_account,
- _mock_issue_api_request):
- _mock_issue_api_request.return_value = self.mock_stats_data
- _mock_create_template_account.return_value = 1
+ def test_delete_volume(self):
testvol = {'project_id': 'testprjid',
'name': 'test_volume',
'size': 1,
'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66',
'created_at': timeutils.utcnow(),
'provider_id': '1 5 None',
+ 'multiattach': True
}
fake_sfaccounts = [{'accountID': 5,
'name': 'testprjid',
'targetSecret': 'shhhh',
'username': 'john-wayne'}]
- def _fake_do_v_create(project_id, params):
- return project_id, params
+ get_vol_result = [{'volumeID': 5,
+ 'name': 'test_volume',
+ 'accountID': 25,
+ 'sliceCount': 1,
+ 'totalSize': 1 * units.Gi,
+ 'enable512e': True,
+ 'access': "readWrite",
+ 'status': "active",
+ 'attributes': {},
+ 'qos': None,
+ 'iqn': 'super_fake_iqn'}]
- sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ mod_conf = self.configuration
+ mod_conf.sf_enable_vag = True
+ sfv = solidfire.SolidFireDriver(configuration=mod_conf)
with mock.patch.object(sfv,
'_get_sfaccounts_for_tenant',
return_value=fake_sfaccounts), \
- mock.patch.object(sfv,
- '_issue_api_request',
- side_effect=self.fake_issue_api_request), \
- mock.patch.object(sfv,
- '_get_account_create_availability',
- return_value=fake_sfaccounts[0]), \
- mock.patch.object(sfv,
- '_do_volume_create',
- side_effect=_fake_do_v_create):
+ mock.patch.object(sfv,
+ '_get_volumes_for_account',
+ return_value=get_vol_result), \
+ mock.patch.object(sfv,
+ '_issue_api_request'), \
+ mock.patch.object(sfv,
+ '_remove_volume_from_vags') as rem_vol:
sfv.delete_volume(testvol)
+ rem_vol.assert_called_with(get_vol_result[0]['volumeID'])
def test_delete_volume_no_volume_on_backend(self):
fake_sfaccounts = [{'accountID': 5,
sfv, '_issue_api_request', side_effect=_fake_issue_api_req):
self.assertEqual(5, sfv._get_sf_volume(test_name, 8)['volumeID'])
- def test_create_vag(self):
- global counter
- counter = 0
-
- def _trick_get_vag(vag_name):
- # On the second call to get_vag we want to return a fake VAG
- # result as required by logic of _sf_initialize_connection.
- global counter
- vag = {'attributes': {},
- 'deletedVolumes': [],
- 'initiators': [],
- 'name': 'TESTIQN',
- 'volumeAccessGroupID': 1,
- 'volumes': [],
- 'virtualNetworkIDs': []}
-
- if counter == 1:
- return [vag]
- counter += 1
-
+ def test_sf_init_conn_with_vag(self):
+ # Verify with the _enable_vag conf set that we correctly create a VAG.
mod_conf = self.configuration
mod_conf.sf_enable_vag = True
sfv = solidfire.SolidFireDriver(configuration=mod_conf)
-
testvol = {'project_id': 'testprjid',
'name': 'testvol',
'size': 1,
'created_at': timeutils.utcnow(),
'provider_id': "1 1 1"
}
-
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
-
- def add_volume_to_vag_check(vol_id, vag_id):
- self.assertEqual(1, vol_id)
- self.assertEqual(1, vag_id)
+ provider_id = testvol['provider_id']
+ vol_id = int(sfv._parse_provider_id_string(provider_id)[0])
+ vag_id = 1
with mock.patch.object(sfv,
- '_create_vag',
- return_value=1), \
- mock.patch.object(sfv,
- '_get_vags',
- side_effect=_trick_get_vag), \
+ '_safe_create_vag',
+ return_value=vag_id) as create_vag, \
mock.patch.object(sfv,
- '_add_initiator_to_vag'), \
- mock.patch.object(sfv,
- '_add_volume_to_vag',
- side_effect=add_volume_to_vag_check):
-
- sfv.initialize_connection(testvol, connector)
-
- def test_remove_vag(self):
- vag = {'attributes': {},
- 'deletedVolumes': [],
- 'initiators': [],
- 'name': 'TESTIQN',
- 'volumeAccessGroupID': 1,
- 'volumes': [1],
- 'virtualNetworkIDs': []}
+ '_add_volume_to_vag') as add_vol:
+ sfv._sf_initialize_connection(testvol, connector)
+ create_vag.assert_called_with(connector['initiator'],
+ vol_id)
+ add_vol.assert_called_with(vol_id,
+ connector['initiator'],
+ vag_id)
+
+ def test_sf_term_conn_with_vag_rem_vag(self):
+ # Verify we correctly remove an empty VAG on detach.
+ mod_conf = self.configuration
+ mod_conf.sf_enable_vag = True
+ sfv = solidfire.SolidFireDriver(configuration=mod_conf)
testvol = {'project_id': 'testprjid',
'name': 'testvol',
'size': 1,
'c76370d66b 2FE0CQ8J196R',
'provider_geometry': '4096 4096',
'created_at': timeutils.utcnow(),
- 'provider_id': "1 1 1"
+ 'provider_id': "1 1 1",
+ 'multiattach': False
}
connector = {'initiator': 'iqn.2012-07.org.fake:01'}
- mod_conf = self.configuration
- mod_conf.sf_enable_vag = True
- sfv = solidfire.SolidFireDriver(configuration=mod_conf)
+ vag_id = 1
+ vags = [{'attributes': {},
+ 'deletedVolumes': [],
+ 'initiators': [connector['initiator']],
+ 'name': 'fakeiqn',
+ 'volumeAccessGroupID': vag_id,
+ 'volumes': [1],
+ 'virtualNetworkIDs': []}]
with mock.patch.object(sfv,
- '_get_vags',
- return_value=[vag]), \
+ '_get_vags_by_name',
+ return_value=vags), \
mock.patch.object(sfv,
- '_remove_vag') as mock_rem_vag:
- sfv.terminate_connection(testvol, connector, force=False)
- mock_rem_vag.assert_called_with(vag['volumeAccessGroupID'])
-
- def test_remove_volume_from_vag(self):
- vag = {'attributes': {},
- 'deletedVolumes': [],
- 'initiators': [],
- 'name': 'TESTIQN',
- 'volumeAccessGroupID': 1,
- 'volumes': [1, 2],
- 'virtualNetworkIDs': []}
+ '_remove_vag') as rem_vag:
+ sfv._sf_terminate_connection(testvol, connector, False)
+ rem_vag.assert_called_with(vag_id)
+
+ def test_sf_term_conn_with_vag_rem_vol(self):
+ # Verify we correctly remove a the volume from a non-empty VAG.
+ mod_conf = self.configuration
+ mod_conf.sf_enable_vag = True
+ sfv = solidfire.SolidFireDriver(configuration=mod_conf)
testvol = {'project_id': 'testprjid',
'name': 'testvol',
'size': 1,
'c76370d66b 2FE0CQ8J196R',
'provider_geometry': '4096 4096',
'created_at': timeutils.utcnow(),
- 'provider_id': "1 1 1"
+ 'provider_id': "1 1 1",
+ 'multiattach': False
}
- connector = {'initiator': 'iqn.2012-07.org.fake:01'}
- mod_conf = self.configuration
- mod_conf.sf_enable_vag = True
- sfv = solidfire.SolidFireDriver(configuration=mod_conf)
provider_id = testvol['provider_id']
vol_id = int(sfv._parse_provider_id_string(provider_id)[0])
+ connector = {'initiator': 'iqn.2012-07.org.fake:01'}
+ vag_id = 1
+ vags = [{'attributes': {},
+ 'deletedVolumes': [],
+ 'initiators': [connector['initiator']],
+ 'name': 'fakeiqn',
+ 'volumeAccessGroupID': vag_id,
+ 'volumes': [1, 2],
+ 'virtualNetworkIDs': []}]
+
+ with mock.patch.object(sfv,
+ '_get_vags_by_name',
+ return_value=vags), \
+ mock.patch.object(sfv,
+ '_remove_volume_from_vag') as rem_vag:
+ sfv._sf_terminate_connection(testvol, connector, False)
+ rem_vag.assert_called_with(vol_id, vag_id)
+
+ def test_safe_create_vag_simple(self):
+ # Test the sunny day call straight into _create_vag.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'fake_iqn'
+ vol_id = 1
+
+ with mock.patch.object(sfv,
+ '_get_vags_by_name',
+ return_value=[]), \
+ mock.patch.object(sfv,
+ '_create_vag') as mock_create_vag:
+ sfv._safe_create_vag(iqn, vol_id)
+ mock_create_vag.assert_called_with(iqn, vol_id)
+
+ def test_safe_create_vag_matching_vag(self):
+ # Vag exists, resuse.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'TESTIQN'
+ vags = [{'attributes': {},
+ 'deletedVolumes': [],
+ 'initiators': [iqn],
+ 'name': iqn,
+ 'volumeAccessGroupID': 1,
+ 'volumes': [1, 2],
+ 'virtualNetworkIDs': []}]
with mock.patch.object(sfv,
- '_get_vags',
- return_value=[vag]), \
+ '_get_vags_by_name',
+ return_value=vags), \
mock.patch.object(sfv,
- '_remove_vag') as mock_rem_vag, \
+ '_create_vag') as create_vag, \
+ mock.patch.object(sfv,
+ '_add_initiator_to_vag') as add_iqn:
+ vag_id = sfv._safe_create_vag(iqn, None)
+ self.assertEqual(vag_id, vags[0]['volumeAccessGroupID'])
+ create_vag.assert_not_called()
+ add_iqn.assert_not_called()
+
+ def test_safe_create_vag_reuse_vag(self):
+ # Reuse a matching vag.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'TESTIQN'
+ vags = [{'attributes': {},
+ 'deletedVolumes': [],
+ 'initiators': [],
+ 'name': iqn,
+ 'volumeAccessGroupID': 1,
+ 'volumes': [1, 2],
+ 'virtualNetworkIDs': []}]
+ vag_id = vags[0]['volumeAccessGroupID']
+
+ with mock.patch.object(sfv,
+ '_get_vags_by_name',
+ return_value=vags), \
+ mock.patch.object(sfv,
+ '_add_initiator_to_vag',
+ return_value = vag_id) as add_init:
+ res_vag_id = sfv._safe_create_vag(iqn, None)
+ self.assertEqual(res_vag_id, vag_id)
+ add_init.assert_called_with(iqn, vag_id)
+
+ def test_create_vag_iqn_fail(self):
+ # Attempt to create a VAG with an already in-use initiator.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'TESTIQN'
+ vag_id = 1
+ vol_id = 42
+
+ def throw_request(method, params, version):
+ msg = 'xExceededLimit: {}'.format(params['initiators'][0])
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request), \
+ mock.patch.object(sfv,
+ '_safe_create_vag',
+ return_value=vag_id) as create_vag, \
+ mock.patch.object(sfv,
+ '_purge_vags') as purge_vags:
+ res_vag_id = sfv._create_vag(iqn, vol_id)
+ self.assertEqual(res_vag_id, vag_id)
+ create_vag.assert_called_with(iqn, vol_id)
+ purge_vags.assert_not_called()
+
+ def test_create_vag_limit_fail(self):
+ # Attempt to create a VAG with VAG limit reached.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'TESTIQN'
+ vag_id = 1
+ vol_id = 42
+
+ def throw_request(method, params, version):
+ msg = 'xExceededLimit'
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request), \
+ mock.patch.object(sfv,
+ '_safe_create_vag',
+ return_value=vag_id) as create_vag, \
+ mock.patch.object(sfv,
+ '_purge_vags') as purge_vags:
+ res_vag_id = sfv._create_vag(iqn, vol_id)
+ self.assertEqual(res_vag_id, vag_id)
+ create_vag.assert_called_with(iqn, vol_id)
+ purge_vags.assert_called_with()
+
+ def test_add_initiator_duplicate(self):
+ # Thrown exception should yield vag_id.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'TESTIQN'
+ vag_id = 1
+
+ def throw_request(method, params, version):
+ msg = 'xAlreadyInVolumeAccessGroup'
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request):
+ res_vag_id = sfv._add_initiator_to_vag(iqn, vag_id)
+ self.assertEqual(vag_id, res_vag_id)
+
+ def test_add_initiator_missing_vag(self):
+ # Thrown exception should result in create_vag call.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'TESTIQN'
+ vag_id = 1
+
+ def throw_request(method, params, version):
+ msg = 'xVolumeAccessGroupIDDoesNotExist'
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request), \
+ mock.patch.object(sfv,
+ '_safe_create_vag',
+ return_value=vag_id) as mock_create_vag:
+ res_vag_id = sfv._add_initiator_to_vag(iqn, vag_id)
+ self.assertEqual(vag_id, res_vag_id)
+ mock_create_vag.assert_called_with(iqn)
+
+ def test_add_volume_to_vag_duplicate(self):
+ # Thrown exception should yield vag_id
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'TESTIQN'
+ vag_id = 1
+ vol_id = 42
+
+ def throw_request(method, params, version):
+ msg = 'xAlreadyInVolumeAccessGroup'
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request):
+ res_vag_id = sfv._add_volume_to_vag(vol_id, iqn, vag_id)
+ self.assertEqual(res_vag_id, vag_id)
+
+ def test_add_volume_to_vag_missing_vag(self):
+ # Thrown exception should yield vag_id
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ iqn = 'TESTIQN'
+ vag_id = 1
+ vol_id = 42
+
+ def throw_request(method, params, version):
+ msg = 'xVolumeAccessGroupIDDoesNotExist'
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request), \
+ mock.patch.object(sfv,
+ '_safe_create_vag',
+ return_value=vag_id) as mock_create_vag:
+ res_vag_id = sfv._add_volume_to_vag(vol_id, iqn, vag_id)
+ self.assertEqual(res_vag_id, vag_id)
+ mock_create_vag.assert_called_with(iqn, vol_id)
+
+ def test_remove_volume_from_vag_missing_volume(self):
+ # Volume not in VAG, throws.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ vag_id = 1
+ vol_id = 42
+
+ def throw_request(method, params, version):
+ msg = 'xNotInVolumeAccessGroup'
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request):
+ sfv._remove_volume_from_vag(vol_id, vag_id)
+
+ def test_remove_volume_from_vag_missing_vag(self):
+ # Volume not in VAG, throws.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ vag_id = 1
+ vol_id = 42
+
+ def throw_request(method, params, version):
+ msg = 'xVolumeAccessGroupIDDoesNotExist'
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request):
+ sfv._remove_volume_from_vag(vol_id, vag_id)
+
+ def test_remove_volume_from_vag_unknown_exception(self):
+ # Volume not in VAG, throws.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ vag_id = 1
+ vol_id = 42
+
+ def throw_request(method, params, version):
+ msg = 'xUnknownException'
+ raise exception.SolidFireAPIException(message=msg)
+
+ with mock.patch.object(sfv,
+ '_issue_api_request',
+ side_effect=throw_request):
+ self.assertRaises(exception.SolidFireAPIException,
+ sfv._remove_volume_from_vag,
+ vol_id,
+ vag_id)
+
+ def test_remove_volume_from_vags(self):
+ # Remove volume from several VAGs.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ vol_id = 42
+ vags = [{'volumeAccessGroupID': 1,
+ 'volumes': [vol_id]},
+ {'volumeAccessGroupID': 2,
+ 'volumes': [vol_id, 43]}]
+
+ with mock.patch.object(sfv,
+ '_base_get_vags',
+ return_value=vags), \
+ mock.patch.object(sfv,
+ '_remove_volume_from_vag') as rem_vol:
+ sfv._remove_volume_from_vags(vol_id)
+ self.assertEqual(len(vags), rem_vol.call_count)
+
+ def test_purge_vags(self):
+ # Remove subset of VAGs.
+ sfv = solidfire.SolidFireDriver(configuration=self.configuration)
+ vags = [{'initiators': [],
+ 'volumeAccessGroupID': 1,
+ 'deletedVolumes': [],
+ 'volumes': [],
+ 'attributes': {'openstack': True}},
+ {'initiators': [],
+ 'volumeAccessGroupID': 2,
+ 'deletedVolumes': [],
+ 'volumes': [],
+ 'attributes': {'openstack': False}},
+ {'initiators': [],
+ 'volumeAccessGroupID': 3,
+ 'deletedVolumes': [1],
+ 'volumes': [],
+ 'attributes': {'openstack': True}},
+ {'initiators': [],
+ 'volumeAccessGroupID': 4,
+ 'deletedVolumes': [],
+ 'volumes': [1],
+ 'attributes': {'openstack': True}},
+ {'initiators': ['fakeiqn'],
+ 'volumeAccessGroupID': 5,
+ 'deletedVolumes': [],
+ 'volumes': [],
+ 'attributes': {'openstack': True}}]
+ with mock.patch.object(sfv,
+ '_base_get_vags',
+ return_value=vags), \
mock.patch.object(sfv,
- '_remove_volume_from_vag') as mock_rem_vol_vag:
- sfv.terminate_connection(testvol, connector, force=False)
- mock_rem_vol_vag.assert_called_with(vol_id,
- vag['volumeAccessGroupID'])
- mock_rem_vag.assert_not_called()
+ '_remove_vag') as rem_vag:
+ sfv._purge_vags()
+ # Of the vags provided there is only one that is valid for purge
+ # based on the limits of no initiators, volumes, deleted volumes,
+ # and features the openstack attribute.
+ self.assertEqual(1, rem_vag.call_count)
+ rem_vag.assert_called_with(1)
CONF = cfg.CONF
CONF.register_opts(sf_opts)
+# SolidFire API Error Constants
+xExceededLimit = 'xExceededLimit'
+xAlreadyInVolumeAccessGroup = 'xAlreadyInVolumeAccessGroup'
+xVolumeAccessGroupIDDoesNotExist = 'xVolumeAccessGroupIDDoesNotExist'
+xNotInVolumeAccessGroup = 'xNotInVolumeAccessGroup'
+
def retry(exc_tuple, tries=5, delay=1, backoff=2):
def retry_dec(f):
vlist = sorted(vlist, key=lambda k: k['volumeID'])
return vlist
- def _create_vag(self, vag_name):
+ def _create_vag(self, iqn, vol_id=None):
"""Create a volume access group(vag).
Returns the vag_id.
"""
- params = {'name': vag_name}
- result = self._issue_api_request('CreateVolumeAccessGroup',
- params,
- version='7.0')
- return result['result']['volumeAccessGroupID']
+ vag_name = re.sub('[^0-9a-zA-Z]+', '-', iqn)
+ params = {'name': vag_name,
+ 'initiators': [iqn],
+ 'volumes': [vol_id],
+ 'attributes': {'openstack': True}}
+ try:
+ result = self._issue_api_request('CreateVolumeAccessGroup',
+ params,
+ version='7.0')
+ return result['result']['volumeAccessGroupID']
+ except exception.SolidFireAPIException as error:
+ if xExceededLimit in error.msg:
+ if iqn in error.msg:
+ # Initiator double registered.
+ return self._safe_create_vag(iqn, vol_id)
+ else:
+ # VAG limit reached. Purge and start over.
+ self._purge_vags()
+ return self._safe_create_vag(iqn, vol_id)
+ else:
+ raise
- def _get_vags(self, vag_name):
- """Retrieve SolidFire volume access group objects by name.
+ def _safe_create_vag(self, iqn, vol_id=None):
+ # Potential race condition with simultaneous volume attaches to the
+ # same host. To help avoid this, VAG creation makes a best attempt at
+ # finding and using an existing VAG.
- Returns an array of vags with a matching name value.
- Returns an empty array if there are no matches.
- """
+ vags = self._get_vags_by_name(iqn)
+ if vags:
+ # Filter through the vags and find the one with matching initiator
+ vag = next((v for v in vags if iqn in v['initiators']), None)
+ if vag:
+ return vag['volumeAccessGroupID']
+ else:
+ # No matches, use the first result, add initiator IQN.
+ vag_id = vags[0]['volumeAccessGroupID']
+ return self._add_initiator_to_vag(iqn, vag_id)
+ return self._create_vag(iqn, vol_id)
+
+ def _base_get_vags(self):
params = {}
vags = self._issue_api_request(
'ListVolumeAccessGroups',
params,
version='7.0')['result']['volumeAccessGroups']
+ return vags
+
+ def _get_vags_by_name(self, iqn):
+ """Retrieve SolidFire volume access group objects by name.
+
+ Returns an array of vags with a matching name value.
+ Returns an empty array if there are no matches.
+ """
+ vags = self._base_get_vags()
+ vag_name = re.sub('[^0-9a-zA-Z]+', '-', iqn)
matching_vags = [vag for vag in vags if vag['name'] == vag_name]
return matching_vags
def _add_initiator_to_vag(self, iqn, vag_id):
+ # Added a vag_id return as there is a chance that we might have to
+ # create a new VAG if our target VAG is deleted underneath us.
params = {"initiators": [iqn],
"volumeAccessGroupID": vag_id}
- self._issue_api_request('AddInitiatorsToVolumeAccessGroup',
- params,
- version='7.0')
+ try:
+ self._issue_api_request('AddInitiatorsToVolumeAccessGroup',
+ params,
+ version='7.0')
+ return vag_id
+ except exception.SolidFireAPIException as error:
+ if xAlreadyInVolumeAccessGroup in error.msg:
+ return vag_id
+ elif xVolumeAccessGroupIDDoesNotExist in error.msg:
+ # No locking means sometimes a VAG can be removed by a parallel
+ # volume detach against the same host.
+ return self._safe_create_vag(iqn)
+ else:
+ raise
- def _add_volume_to_vag(self, vol_id, vag_id):
+ def _add_volume_to_vag(self, vol_id, iqn, vag_id):
+ # Added a vag_id return to be consistent with add_initiator_to_vag. It
+ # isn't necessary but may be helpful in the future.
params = {"volumeAccessGroupID": vag_id,
"volumes": [vol_id]}
- self._issue_api_request('AddVolumesToVolumeAccessGroup',
- params,
- version='7.0')
+ try:
+ self._issue_api_request('AddVolumesToVolumeAccessGroup',
+ params,
+ version='7.0')
+ return vag_id
+
+ except exception.SolidFireAPIException as error:
+ if xAlreadyInVolumeAccessGroup in error.msg:
+ return vag_id
+ elif xVolumeAccessGroupIDDoesNotExist in error.msg:
+ return self._safe_create_vag(iqn, vol_id)
+ else:
+ raise
def _remove_volume_from_vag(self, vol_id, vag_id):
params = {"volumeAccessGroupID": vag_id,
"volumes": [vol_id]}
- self._issue_api_request('RemoveVolumesFromVolumeAccessGroup',
- params,
- version='7.0')
+ try:
+ self._issue_api_request('RemoveVolumesFromVolumeAccessGroup',
+ params,
+ version='7.0')
+ except exception.SolidFireAPIException as error:
+ if xNotInVolumeAccessGroup in error.msg:
+ pass
+ elif xVolumeAccessGroupIDDoesNotExist in error.msg:
+ pass
+ else:
+ raise
+
+ def _remove_volume_from_vags(self, vol_id):
+ # Due to all sorts of uncertainty around multiattach, on volume
+ # deletion we make a best attempt at removing the vol_id from VAGs.
+ vags = self._base_get_vags()
+ targets = [v for v in vags if vol_id in v['volumes']]
+ for vag in targets:
+ self._remove_volume_from_vag(vol_id, vag['volumeAccessGroupID'])
def _remove_vag(self, vag_id):
params = {"volumeAccessGroupID": vag_id}
- self._issue_api_request('DeleteVolumeAccessGroup',
- params,
- version='7.0')
+ try:
+ self._issue_api_request('DeleteVolumeAccessGroup',
+ params,
+ version='7.0')
+ except exception.SolidFireAPIException as error:
+ if xVolumeAccessGroupIDDoesNotExist not in error.msg:
+ raise
+
+ def _purge_vags(self, limit=10):
+ # Purge up to limit number of VAGs that have no active volumes,
+ # initiators, and an OpenStack attribute. Purge oldest VAGs first.
+ vags = self._base_get_vags()
+ targets = [v for v in vags if v['volumes'] == [] and
+ v['initiators'] == [] and
+ v['deletedVolumes'] == [] and
+ v['attributes'].get('openstack')]
+ sorted_targets = sorted(targets,
+ key=lambda k: k['volumeAccessGroupID'])
+ for vag in sorted_targets[:limit]:
+ self._remove_vag(vag['volumeAccessGroupID'])
def clone_image(self, context,
volume, image_location,
if sf_vol is not None:
params = {'volumeID': sf_vol['volumeID']}
self._issue_api_request('DeleteVolume', params)
+ if volume.get('multiattach'):
+ self._remove_volume_from_vags(sf_vol['volumeID'])
else:
LOG.error(_LE("Volume ID %s was not found on "
"the SolidFire Cluster while attempting "
Optionally checks and utilizes volume access groups.
"""
if self.configuration.sf_enable_vag:
- raw_iqn = connector['initiator']
- vag_name = re.sub('[^0-9a-zA-Z]+', '-', raw_iqn)
- vag = self._get_vags(vag_name)
+ iqn = connector['initiator']
provider_id = volume['provider_id']
vol_id = int(self._parse_provider_id_string(provider_id)[0])
- if vag:
- vag_id = vag[0]['volumeAccessGroupID']
- vag = vag[0]
- else:
- vag_id = self._create_vag(vag_name)
- vag = self._get_vags(vag_name)[0]
-
- # TODO(chrismorrell): There is a potential race condition if a
- # volume is attached and another is detached on the same
- # host/initiator. The detach may purge the VAG before the attach
- # has a chance to add the volume to the VAG. Propose combining
- # add_initiator_to_vag and add_volume_to_vag with a retry on
- # SFAPI exception regarding nonexistent VAG.
-
- # Verify IQN matches.
- if raw_iqn not in vag['initiators']:
- self._add_initiator_to_vag(raw_iqn,
- vag_id)
- # Add volume to vag if not already.
- if vol_id not in vag['volumes']:
- self._add_volume_to_vag(vol_id, vag_id)
+ # safe_create_vag may opt to reuse vs create a vag, so we need to
+ # add our vol_id.
+ vag_id = self._safe_create_vag(iqn, vol_id)
+ self._add_volume_to_vag(vol_id, iqn, vag_id)
# Continue along with default behavior
return super(SolidFireISCSI, self).initialize_connection(volume,
If the VAG is empty then the VAG is also removed.
"""
if self.configuration.sf_enable_vag:
- raw_iqn = properties['initiator']
- vag_name = re.sub('[^0-9a-zA-Z]+', '-', raw_iqn)
- vag = self._get_vags(vag_name)
+ iqn = properties['initiator']
+ vag = self._get_vags_by_name(iqn)
provider_id = volume['provider_id']
vol_id = int(self._parse_provider_id_string(provider_id)[0])
- if vag:
+ if vag and not volume['multiattach']:
+ # Multiattach causes problems with removing volumes from VAGs.
+ # Compromise solution for now is to remove multiattach volumes
+ # from VAGs during volume deletion.
vag = vag[0]
vag_id = vag['volumeAccessGroupID']
if [vol_id] == vag['volumes']: