diff --git a/cinder/tests/unit/test_solidfire.py b/cinder/tests/unit/test_solidfire.py index a7bfc1312bd..f788ee72f33 100644 --- a/cinder/tests/unit/test_solidfire.py +++ b/cinder/tests/unit/test_solidfire.py @@ -180,6 +180,8 @@ class SolidFireVolumeTestCase(test.TestCase): 'qos': None, 'iqn': test_name}]}} return result + elif method is 'DeleteSnapshot': + return {'result': {}} else: # Crap, unimplemented API call in Fake return None @@ -206,13 +208,13 @@ class SolidFireVolumeTestCase(test.TestCase): def fake_get_model_info(self, account, vid): return {'fake': 'fake-model'} - def test_create_with_qos_type(self): - self.stubs.Set(solidfire.SolidFireDriver, - '_issue_api_request', - self.fake_issue_api_request) - self.stubs.Set(solidfire.SolidFireDriver, - '_set_qos_by_volume_type', - self.fake_set_qos_by_volume_type) + @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request') + @mock.patch.object(solidfire.SolidFireDriver, '_create_template_account') + def test_create_volume_with_qos_type(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 testvol = {'project_id': 'testprjid', 'name': 'testvol', 'size': 1, @@ -220,52 +222,128 @@ class SolidFireVolumeTestCase(test.TestCase): 'volume_type_id': 'fast', 'created_at': timeutils.utcnow()} - sfv = solidfire.SolidFireDriver(configuration=self.configuration) - model_update = sfv.create_volume(testvol) - self.assertIsNotNone(model_update) + fake_sfaccounts = [{'accountID': 5, + 'name': 'testprjid', + 'targetSecret': 'shhhh', + 'username': 'john-wayne'}] - def test_create_volume(self): - self.stubs.Set(solidfire.SolidFireDriver, - '_issue_api_request', - self.fake_issue_api_request) + test_type = {'name': 'sf-1', + 'qos_specs_id': 'fb0576d7-b4b5-4cad-85dc-ca92e6a497d1', + 'deleted': False, + 'created_at': '2014-02-06 04:58:11', + 'updated_at': None, + 'extra_specs': {}, + 'deleted_at': None, + 'id': 'e730e97b-bc7d-4af3-934a-32e59b218e81'} + + test_qos_spec = {'id': 'asdfafdasdf', + 'specs': {'minIOPS': '1000', + 'maxIOPS': '2000', + 'burstIOPS': '3000'}} + + def _fake_get_volume_type(ctxt, type_id): + return test_type + + def _fake_get_qos_spec(ctxt, spec_id): + return test_qos_spec + + def _fake_do_volume_create(account, params): + return params + + sfv = solidfire.SolidFireDriver(configuration=self.configuration) + 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_volume_create), \ + mock.patch.object(volume_types, + 'get_volume_type', + side_effect=_fake_get_volume_type), \ + mock.patch.object(qos_specs, + 'get_qos_specs', + side_effect=_fake_get_qos_spec): + + self.assertEqual({'burstIOPS': 3000, + 'minIOPS': 1000, + 'maxIOPS': 2000}, + sfv.create_volume(testvol)['qos']) + + @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request') + @mock.patch.object(solidfire.SolidFireDriver, '_create_template_account') + def test_create_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 testvol = {'project_id': 'testprjid', 'name': 'testvol', 'size': 1, 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', 'volume_type_id': None, 'created_at': timeutils.utcnow()} + fake_sfaccounts = [{'accountID': 5, + 'name': 'testprjid', + 'targetSecret': 'shhhh', + 'username': 'john-wayne'}] sfv = solidfire.SolidFireDriver(configuration=self.configuration) - model_update = sfv.create_volume(testvol) - self.assertIsNotNone(model_update) - self.assertIsNone(model_update.get('provider_geometry', None)) + 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]): - def test_create_volume_non_512(self): - self.stubs.Set(solidfire.SolidFireDriver, - '_issue_api_request', - self.fake_issue_api_request) + model_update = sfv.create_volume(testvol) + self.assertIsNotNone(model_update) + self.assertIsNone(model_update.get('provider_geometry', None)) + + @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request') + @mock.patch.object(solidfire.SolidFireDriver, '_create_template_account') + def test_create_volume_non_512e(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 testvol = {'project_id': 'testprjid', 'name': 'testvol', 'size': 1, 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', 'volume_type_id': None, 'created_at': timeutils.utcnow()} + fake_sfaccounts = [{'accountID': 5, + 'name': 'testprjid', + 'targetSecret': 'shhhh', + 'username': 'john-wayne'}] - self.configuration.sf_emulate_512 = False sfv = solidfire.SolidFireDriver(configuration=self.configuration) - model_update = sfv.create_volume(testvol) - self.assertEqual(model_update.get('provider_geometry', None), - '4096 4096') - self.configuration.sf_emulate_512 = True + 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]): + + self.configuration.sf_emulate_512 = False + model_update = sfv.create_volume(testvol) + self.configuration.sf_emulate_512 = True + self.assertEqual(model_update.get('provider_geometry', None), + '4096 4096') def test_create_delete_snapshot(self): - testvol = {'project_id': 'testprjid', - 'name': 'testvol', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', - 'volume_type_id': None, - 'created_at': timeutils.utcnow()} - testsnap = {'project_id': 'testprjid', 'name': 'testvol', 'volume_size': 1, @@ -275,21 +353,26 @@ class SolidFireVolumeTestCase(test.TestCase): 'created_at': timeutils.utcnow()} sfv = solidfire.SolidFireDriver(configuration=self.configuration) - sfv.create_volume(testvol) sfv.create_snapshot(testsnap) with mock.patch.object(solidfire.SolidFireDriver, '_get_sf_snapshots', return_value=[{'snapshotID': '1', - 'name': 'testvol'}]): + 'name': 'UUID-b831c4d1-d1f0-11e1-9b23-0800200c9a66'}]), \ + mock.patch.object(sfv, + '_get_sfaccounts_for_tenant', + return_value=[{'accountID': 5, + 'name': 'testprjid'}]): sfv.delete_snapshot(testsnap) - def test_create_clone(self): - self.stubs.Set(solidfire.SolidFireDriver, - '_issue_api_request', - self.fake_issue_api_request) - self.stubs.Set(solidfire.SolidFireDriver, - '_get_model_info', - self.fake_get_model_info) + @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request') + @mock.patch.object(solidfire.SolidFireDriver, '_create_template_account') + def test_create_clone(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 + _fake_get_snaps = [{'snapshotID': 5, 'name': 'testvol'}] + testvol = {'project_id': 'testprjid', 'name': 'testvol', 'size': 1, @@ -304,10 +387,19 @@ class SolidFireVolumeTestCase(test.TestCase): 'volume_type_id': None, 'created_at': timeutils.utcnow()} - with mock.patch.object(solidfire.SolidFireDriver, + sfv = solidfire.SolidFireDriver(configuration=self.configuration) + with mock.patch.object(sfv, '_get_sf_snapshots', - return_value=[]): - sfv = solidfire.SolidFireDriver(configuration=self.configuration) + return_value=_fake_get_snaps), \ + mock.patch.object(sfv, + '_issue_api_request', + side_effect=self.fake_issue_api_request), \ + mock.patch.object(sfv, + '_get_sfaccounts_for_tenant', + return_value=[]), \ + mock.patch.object(sfv, + '_get_model_info', + return_value={}): sfv.create_cloned_volume(testvol_b, testvol) def test_initialize_connector_with_blocksizes(self): @@ -331,25 +423,6 @@ class SolidFireVolumeTestCase(test.TestCase): self.assertEqual('4096', properties['data']['physical_block_size']) self.assertEqual('4096', properties['data']['logical_block_size']) - def test_create_volume_with_qos(self): - preset_qos = {} - preset_qos['qos'] = 'fast' - self.stubs.Set(solidfire.SolidFireDriver, - '_issue_api_request', - self.fake_issue_api_request) - - testvol = {'project_id': 'testprjid', - 'name': 'testvol', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', - 'metadata': [preset_qos], - 'volume_type_id': None, - 'created_at': timeutils.utcnow()} - - sfv = solidfire.SolidFireDriver(configuration=self.configuration) - model_update = sfv.create_volume(testvol) - self.assertIsNotNone(model_update) - def test_create_volume_fails(self): # NOTE(JDG) This test just fakes update_cluster_status # this is inentional for this test @@ -403,18 +476,41 @@ class SolidFireVolumeTestCase(test.TestCase): account = sfv._get_sfaccount_by_name('some-name') self.assertIsNone(account) - def test_delete_volume(self): - self.stubs.Set(solidfire.SolidFireDriver, - '_issue_api_request', - self.fake_issue_api_request) + @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 testvol = {'project_id': 'testprjid', 'name': 'test_volume', 'size': 1, 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', 'created_at': timeutils.utcnow()} + fake_sfaccounts = [{'accountID': 5, + 'name': 'testprjid', + 'targetSecret': 'shhhh', + 'username': 'john-wayne'}] + + def _fake_do_v_create(project_id, params): + return project_id, params sfv = solidfire.SolidFireDriver(configuration=self.configuration) - sfv.delete_volume(testvol) + 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): + + sfv.delete_volume(testvol) def test_delete_volume_fails_no_volume(self): self.stubs.Set(solidfire.SolidFireDriver, @@ -433,26 +529,6 @@ class SolidFireVolumeTestCase(test.TestCase): except Exception: pass - def test_delete_volume_fails_account_lookup(self): - # NOTE(JDG) This test just fakes update_cluster_status - # this is inentional for this test - self.stubs.Set(solidfire.SolidFireDriver, - '_update_cluster_status', - self.fake_update_cluster_status) - self.stubs.Set(solidfire.SolidFireDriver, - '_issue_api_request', - self.fake_issue_api_request_fails) - testvol = {'project_id': 'testprjid', - 'name': 'no-name', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', - 'created_at': timeutils.utcnow()} - - sfv = solidfire.SolidFireDriver(configuration=self.configuration) - self.assertRaises(exception.SolidFireAccountNotFound, - sfv.delete_volume, - testvol) - def test_get_cluster_info(self): self.stubs.Set(solidfire.SolidFireDriver, '_issue_api_request', @@ -690,16 +766,13 @@ class SolidFireVolumeTestCase(test.TestCase): self.assertIsNotNone(model_update) self.assertIsNone(model_update.get('provider_geometry', None)) - def test_create_volume_for_migration(self): - def _fake_do_v_create(self, project_id, params): - return project_id, params - - self.stubs.Set(solidfire.SolidFireDriver, - '_issue_api_request', - self.fake_issue_api_request) - self.stubs.Set(solidfire.SolidFireDriver, - '_do_volume_create', _fake_do_v_create) - + @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request') + @mock.patch.object(solidfire.SolidFireDriver, '_create_template_account') + def test_create_volume_for_migration(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 testvol = {'project_id': 'testprjid', 'name': 'testvol', 'size': 1, @@ -708,15 +781,35 @@ class SolidFireVolumeTestCase(test.TestCase): 'created_at': timeutils.utcnow(), 'migration_status': 'target:' 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} + fake_sfaccounts = [{'accountID': 5, + 'name': 'testprjid', + 'targetSecret': 'shhhh', + 'username': 'john-wayne'}] + + def _fake_do_v_create(project_id, params): + return project_id, params sfv = solidfire.SolidFireDriver(configuration=self.configuration) - proj_id, sf_vol_object = sfv.create_volume(testvol) - self.assertEqual('a720b3c0-d1f0-11e1-9b23-0800200c9a66', - sf_vol_object['attributes']['uuid']) - self.assertEqual('b830b3c0-d1f0-11e1-9b23-1900200c9a77', - sf_vol_object['attributes']['migration_uuid']) - self.assertEqual('UUID-a720b3c0-d1f0-11e1-9b23-0800200c9a66', - sf_vol_object['name']) + 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): + + proj_id, sf_vol_object = sfv.create_volume(testvol) + self.assertEqual('a720b3c0-d1f0-11e1-9b23-0800200c9a66', + sf_vol_object['attributes']['uuid']) + self.assertEqual('b830b3c0-d1f0-11e1-9b23-1900200c9a77', + sf_vol_object['attributes']['migration_uuid']) + self.assertEqual('UUID-a720b3c0-d1f0-11e1-9b23-0800200c9a66', + sf_vol_object['name']) @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request') @mock.patch.object(solidfire.SolidFireDriver, '_get_sfaccount') @@ -808,9 +901,10 @@ class SolidFireVolumeTestCase(test.TestCase): self.fake_image_meta, 'fake')) - @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request') - def test_clone_image_authorization(self, _mock_issue_api_request): - _mock_issue_api_request.return_value = self.mock_stats_data + @mock.patch.object(solidfire.SolidFireDriver, '_create_template_account') + def test_clone_image_authorization(self, _mock_create_template_account): + _mock_create_template_account.return_value = 1 + self.configuration.sf_allow_template_caching = True sfv = solidfire.SolidFireDriver(configuration=self.configuration) @@ -824,33 +918,40 @@ class SolidFireVolumeTestCase(test.TestCase): 'properties': {'virtual_size': 1}, 'is_public': False, 'owner': 'wrong-owner'} - self.assertEqual((None, False), - sfv.clone_image(self.ctxt, - self.mock_volume, - 'fake', - _fake_image_meta, - 'fake')) + with mock.patch.object(sfv, '_do_clone_volume', + return_value=('fe', 'fi', 'fo')): + self.assertEqual((None, False), + sfv.clone_image(self.ctxt, + self.mock_volume, + 'fake', + _fake_image_meta, + 'fake')) - # And is_public False, but the correct owner does work - # expect raise AccountNotFound as that's the next call after - # auth checks - _fake_image_meta['owner'] = 'testprjid' - self.assertRaises(exception.SolidFireAccountNotFound, - sfv.clone_image, self.ctxt, - self.mock_volume, 'fake', - _fake_image_meta, 'fake') + # And is_public False, but the correct owner does work + _fake_image_meta['owner'] = 'testprjid' + self.assertEqual(('fo', True), sfv.clone_image(self.ctxt, + self.mock_volume, + 'fake', + _fake_image_meta, + 'fake')) - # And is_public True, even if not the correct owner - _fake_image_meta['is_public'] = True - _fake_image_meta['owner'] = 'wrong-owner' - self.assertRaises(exception.SolidFireAccountNotFound, - sfv.clone_image, self.ctxt, - self.mock_volume, 'fake', - _fake_image_meta, 'fake') + # And is_public True, even if not the correct owner + _fake_image_meta['is_public'] = True + _fake_image_meta['owner'] = 'wrong-owner' + self.assertEqual(('fo', True), sfv.clone_image(self.ctxt, + self.mock_volume, + 'fake', + _fake_image_meta, + 'fake')) @mock.patch.object(solidfire.SolidFireDriver, '_issue_api_request') - def test_clone_image_virt_size_not_set(self, _mock_issue_api_request): + @mock.patch.object(solidfire.SolidFireDriver, '_create_template_account') + def test_clone_image_virt_size_not_set(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 + self.configuration.sf_allow_template_caching = True sfv = solidfire.SolidFireDriver(configuration=self.configuration) diff --git a/cinder/volume/drivers/solidfire.py b/cinder/volume/drivers/solidfire.py index 43864665df6..a4ef009be9d 100644 --- a/cinder/volume/drivers/solidfire.py +++ b/cinder/volume/drivers/solidfire.py @@ -112,10 +112,11 @@ class SolidFireDriver(san.SanISCSIDriver): 1.2.2 - Catch VolumeNotFound on accept xfr 2.0.0 - Move from httplib to requests 2.0.1 - Implement SolidFire Snapshots + 2.0.2 - Implement secondary account """ - VERSION = '2.0.1' + VERSION = '2.0.2' sf_qos_dict = {'slow': {'minIOPS': 100, 'maxIOPS': 200, @@ -146,27 +147,29 @@ class SolidFireDriver(san.SanISCSIDriver): super(SolidFireDriver, self).__init__(*args, **kwargs) self.configuration.append_config_values(sf_opts) self._endpoint = self._build_endpoint_info() + self.template_account_id = None + self.max_volumes_per_account = 1990 try: self._update_cluster_status() except exception.SolidFireAPIException: pass if self.configuration.sf_allow_template_caching: account = self.configuration.sf_template_account_name - self._create_template_account(account) + self.template_account_id = self._create_template_account(account) def _create_template_account(self, account_name): - chap_secret = self._generate_random_string(12) - params = {'username': account_name, - 'initiatorSecret': chap_secret, - 'targetSecret': chap_secret, - 'attributes': {}} - try: - self._issue_api_request('AddAccount', params) - except exception.SolidFireAPIException as ex: - if 'DuplicateUsername' in ex.msg: - pass - else: - raise + id = self._issue_api_request( + 'GetAccountByName', + {'username': account_name})['result']['account']['accountID'] + if not id: + chap_secret = self._generate_random_string(12) + params = {'username': account_name, + 'initiatorSecret': chap_secret, + 'targetSecret': chap_secret, + 'attributes': {}} + id = self._issue_api_request('AddAccount', + params)['result']['accountID'] + return id def _build_endpoint_info(self, **kwargs): endpoint = {} @@ -347,64 +350,59 @@ class SolidFireDriver(san.SanISCSIDriver): def _do_clone_volume(self, src_uuid, src_project_id, - v_ref): - """Create a clone of an existing volume. - - Currently snapshots are the same as clones on the SF cluster. - Due to the way the SF cluster works there's no loss in efficiency - or space usage between the two. The only thing different right - now is the restore snapshot functionality which has not been - implemented in the pre-release version of the SolidFire Cluster. - - """ - + vref): + """Create a clone of an existing volume or snapshot. """ attributes = {} qos = {} - sfaccount = self._get_sfaccount(src_project_id) - if src_project_id != v_ref['project_id']: - sfaccount = self._create_sfaccount(v_ref['project_id']) - - if v_ref.get('size', None): - new_size = v_ref['size'] + sf_accounts = self._get_sfaccounts_for_tenant(vref['project_id']) + if not sf_accounts: + sf_account = self._create_sfaccount(vref['project_id']) else: - new_size = v_ref['volume_size'] + # Check availability for creates + sf_account = self._get_account_create_availability(sf_accounts) + if not sf_account: + # TODO(jdg): We're not doing tertiaries, so fail + msg = _('volumes/account exceeded on both primary ' + 'and secondary SolidFire accounts') + raise exception.SolidFireDriverException(msg) - params = {'name': 'UUID-%s' % v_ref['id'], - 'newSize': int(new_size * units.Gi), - 'newAccountID': sfaccount['accountID']} + params = {'name': 'UUID-%s' % vref['id'], + 'newAccountID': sf_account['accountID']} # NOTE(jdg): First check the SF snapshots # if we don't find a snap by the given name, just move on to check # volumes. This may be a running system that was updated from # before we did snapshots, so need to check both + is_clone = False snap_name = 'UUID-%s' % src_uuid snaps = self._get_sf_snapshots() snap = next((s for s in snaps if s["name"] == snap_name), None) - is_clone = False if snap: params['snapshotID'] = int(snap['snapshotID']) params['volumeID'] = int(snap['volumeID']) + params['newSize'] = int(vref['size'] * units.Gi) else: sf_vol = self._get_sf_volume( - src_uuid, {'accountID': sfaccount['accountID']}) + src_uuid, {'accountID': sf_account['accountID']}) if sf_vol is None: raise exception.VolumeNotFound(volume_id=src_uuid) params['volumeID'] = int(sf_vol['volumeID']) + params['newSize'] = int(vref['size'] * units.Gi) is_clone = True data = self._issue_api_request('CloneVolume', params, version='6.0') if (('result' not in data) or ('volumeID' not in data['result'])): msg = _("API response: %s") % data raise exception.SolidFireAPIException(msg) - sf_volume_id = data['result']['volumeID'] + sf_volume_id = data['result']['volumeID'] if (self.configuration.sf_allow_tenant_qos and - v_ref.get('volume_metadata')is not None): - qos = self._set_qos_presets(v_ref) + vref.get('volume_metadata')is not None): + qos = self._set_qos_presets(vref) ctxt = context.get_admin_context() - type_id = v_ref.get('volume_type_id', None) + type_id = vref.get('volume_type_id', None) if type_id is not None: qos = self._set_qos_by_volume_type(ctxt, type_id) @@ -412,8 +410,8 @@ class SolidFireDriver(san.SanISCSIDriver): # to set any that were provided params = {'volumeID': sf_volume_id} - create_time = v_ref['created_at'].isoformat() - attributes = {'uuid': v_ref['id'], + create_time = vref['created_at'].isoformat() + attributes = {'uuid': vref['id'], 'is_clone': 'True', 'src_uuid': src_uuid, 'created_at': create_time} @@ -425,7 +423,7 @@ class SolidFireDriver(san.SanISCSIDriver): params['attributes'] = attributes data = self._issue_api_request('ModifyVolume', params) - model_update = self._get_model_info(sfaccount, sf_volume_id) + model_update = self._get_model_info(sf_account, sf_volume_id) if model_update is None: mesg = _('Failed to get model update from clone') raise exception.SolidFireAPIException(mesg) @@ -433,27 +431,27 @@ class SolidFireDriver(san.SanISCSIDriver): # Increment the usage count, just for data collection # We're only doing this for clones, not create_from snaps if is_clone: - cloned_count = sf_vol['attributes'].get('cloned_count', 0) - cloned_count += 1 - attributes = sf_vol['attributes'] - attributes['cloned_count'] = cloned_count + data = self._update_attributes(sf_vol) + return (data, sf_account, model_update) - params = {'volumeID': int(sf_vol['volumeID'])} - params['attributes'] = attributes - data = self._issue_api_request('ModifyVolume', params) - return (data, sfaccount, model_update) + def _update_attributes(self, sf_vol): + cloned_count = sf_vol['attributes'].get('cloned_count', 0) + cloned_count += 1 + attributes = sf_vol['attributes'] + attributes['cloned_count'] = cloned_count - def _do_volume_create(self, project_id, params): - sfaccount = self._create_sfaccount(project_id) - params['accountID'] = sfaccount['accountID'] + params = {'volumeID': int(sf_vol['volumeID'])} + params['attributes'] = attributes + return self._issue_api_request('ModifyVolume', params) + + def _do_volume_create(self, sf_account, params): data = self._issue_api_request('CreateVolume', params) - if (('result' not in data) or ('volumeID' not in data['result'])): msg = _("Failed volume create: %s") % data raise exception.SolidFireAPIException(msg) sf_volume_id = data['result']['volumeID'] - return self._get_model_info(sfaccount, sf_volume_id) + return self._get_model_info(sf_account, sf_volume_id) def _do_snapshot_create(self, params): data = self._issue_api_request('CreateSnapshot', params, version='6.0') @@ -569,17 +567,19 @@ class SolidFireDriver(san.SanISCSIDriver): attributes['image_info']['image_created_at'] =\ image_meta['created_at'].isoformat() attributes['image_info']['image_id'] = image_meta['id'] - params = {'name': 'OpenStackIMG-%s' % image_id, - 'accountID': None, + 'accountID': self.template_account_id, 'sliceCount': 1, 'totalSize': int(min_sz_in_bytes), 'enable512e': self.configuration.sf_emulate_512, 'attributes': attributes, 'qos': {}} - account = self.configuration.sf_template_account_name - template_vol = self._do_volume_create(account, params) + sf_account = self._issue_api_request( + 'GetAccountByID', + {'accountID': self.template_account_id}) + + template_vol = self._do_volume_create(sf_account, params) tvol = {} tvol['id'] = image_id tvol['provider_location'] = template_vol['provider_location'] @@ -588,9 +588,6 @@ class SolidFireDriver(san.SanISCSIDriver): connector = 'na' conn = self.initialize_connection(tvol, connector) attach_info = super(SolidFireDriver, self)._connect_device(conn) - - sfaccount = self._get_sfaccount(account) - params = {'accountID': sfaccount['accountID']} properties = 'na' try: @@ -625,10 +622,7 @@ class SolidFireDriver(san.SanISCSIDriver): # If it's out of date, just delete it and we'll create a new one # Any other case we don't care and just return without doing anything - sfaccount = self._get_sfaccount( - self.configuration.sf_template_account_name) - - params = {'accountID': sfaccount['accountID']} + params = {'accountID': self.template_account_id} sf_vol = self._get_sf_volume(image_meta['id'], params) if sf_vol is None: return @@ -639,7 +633,7 @@ class SolidFireDriver(san.SanISCSIDriver): return else: # Bummer, it's been updated, delete it - params = {'accountID': sfaccount['accountID']} + params = {'accountID': self.template_account_id} params['volumeID'] = sf_vol['volumeID'] data = self._issue_api_request('DeleteVolume', params) if 'result' not in data: @@ -653,6 +647,75 @@ class SolidFireDriver(san.SanISCSIDriver): msg = _("Failed to create SolidFire Image-Volume") raise exception.SolidFireAPIException(msg) + def _get_sfaccounts_for_tenant(self, cinder_project_id): + data = self._issue_api_request('ListAccounts', {}) + if 'result' not in data: + msg = _("API response: %s") % data + raise exception.SolidFireAPIException(msg) + + # Note(jdg): On SF we map account-name to OpenStack's tenant ID + # we use tenantID in here to get secondaries that might exist + # Also: we expect this to be sorted, so we get the primary first + # in the list + return sorted([acc for acc in data['result']['accounts'] if + cinder_project_id in acc['username']]) + + def _get_all_active_volumes(self, cinder_uuid=None): + params = {} + data = self._issue_api_request('ListActiveVolumes', + params) + if 'result' not in data: + msg = _("Failed get active SolidFire volumes: %s") % data + raise exception.SolidFireAPIException(msg) + if cinder_uuid: + deleted_vols = ([v for v in data['result']['volumes'] if + cinder_uuid in v.name]) + else: + deleted_vols = [v for v in data['result']['volumes']] + return deleted_vols + + def _get_all_deleted_volumes(self, cinder_uuid=None): + params = {} + data = self._issue_api_request('ListDeletedVolumes', + params) + if 'result' not in data: + msg = _("Failed get Deleted SolidFire volumes: %s") % data + raise exception.SolidFireAPIException(msg) + if cinder_uuid: + deleted_vols = ([v for v in data['result']['volumes'] if + cinder_uuid in v['name']]) + else: + deleted_vols = [v for v in data['result']['volumes']] + return deleted_vols + + def _get_account_create_availability(self, accounts): + # we'll check both the primary and the secondary + # if it exists and return whichever one has count + # available. + for acc in accounts: + if self._get_volumes_for_account( + acc['accountID']) > self.max_volumes_per_account: + return acc + if len(accounts) == 1: + sfaccount = self._create_sfaccount(accounts[0]['name'] + '_') + return sfaccount + return None + + def _get_volumes_for_account(self, sf_account_id, cinder_uuid=None): + # ListVolumesForAccount gives both Active and Deleted + # we require the solidfire accountID, uuid of volume + # is optional + params = {'accountID': sf_account_id} + response = self._issue_api_request('ListVolumesForAccount', + params) + if cinder_uuid: + vlist = [v for v in response['result']['volumes'] if + cinder_uuid in v['name']] + else: + vlist = [v for v in response['result']['volumes']] + vlist = sorted(vlist, key=lambda k: k['volumeID']) + return vlist + def clone_image(self, context, volume, image_location, image_meta, image_service): @@ -739,8 +802,14 @@ class SolidFireDriver(san.SanISCSIDriver): for k, v in qos.items(): attributes[k] = str(v) + sf_accounts = self._get_sfaccounts_for_tenant(volume['project_id']) + if not sf_accounts: + sf_account = self._create_sfaccount(volume['project_id']) + else: + sf_account = self._get_account_create_availability(sf_accounts) + params = {'name': 'UUID-%s' % volume['id'], - 'accountID': None, + 'accountID': sf_account['accountID'], 'sliceCount': slice_count, 'totalSize': int(volume['size'] * units.Gi), 'enable512e': self.configuration.sf_emulate_512, @@ -755,8 +824,7 @@ class SolidFireDriver(san.SanISCSIDriver): params['name'] = 'UUID-%s' % v params['attributes']['migration_uuid'] = volume['id'] params['attributes']['uuid'] = v - - return self._do_volume_create(volume['project_id'], params) + return self._do_volume_create(sf_account, params) def create_cloned_volume(self, volume, src_vref): """Create a clone of an existing volume.""" @@ -774,8 +842,8 @@ class SolidFireDriver(san.SanISCSIDriver): volumeID is what's guaranteed unique. """ - sfaccount = self._get_sfaccount(volume['project_id']) - if sfaccount is None: + accounts = self._get_sfaccounts_for_tenant(volume['project_id']) + if accounts is None: LOG.error(_LE("Account for Volume ID %s was not found on " "the SolidFire Cluster while attempting " "delete_volume operation!"), volume['id']) @@ -783,8 +851,11 @@ class SolidFireDriver(san.SanISCSIDriver): "successfully created.")) return - params = {'accountID': sfaccount['accountID']} - sf_vol = self._get_sf_volume(volume['id'], params) + for acc in accounts: + sf_vol = self._get_volumes_for_account(acc['accountID'], + volume['id'])[0] + if sf_vol: + break if sf_vol is not None: params = {'volumeID': sf_vol['volumeID']} @@ -812,28 +883,27 @@ class SolidFireDriver(san.SanISCSIDriver): def delete_snapshot(self, snapshot): """Delete the specified snapshot from the SolidFire cluster.""" sf_snap_name = 'UUID-%s' % snapshot['id'] - sfaccount = self._get_sfaccount(snapshot['project_id']) - params = {'accountID': sfaccount['accountID'], - 'name': sf_snap_name} - params = {'accountID': sfaccount['accountID']} - - # Get the parent volume of the snapshot - sf_vol = self._get_sf_volume(snapshot['volume_id'], params) - sf_snaps = self._get_sf_snapshots(sf_vol['volumeID']) - snap = next((s for s in sf_snaps if s["name"] == sf_snap_name), None) - if snap: - params = {'snapshotID': snap['snapshotID']} - data = self._issue_api_request('DeleteSnapshot', - params, - version='6.0') - if 'result' not in data: - msg = (_("Failed to delete SolidFire Snapshot: %s") % - data) - raise exception.SolidFireAPIException(msg) - else: - # Make sure it's not "old style" using clones as snaps - LOG.debug("Snapshot not found, checking old style clones.") - self.delete_volume(snapshot) + accounts = self._get_sfaccounts_for_tenant(snapshot['project_id']) + snap = None + for a in accounts: + params = {'accountID': a['accountID']} + sf_vol = self._get_sf_volume(snapshot['volume_id'], params) + sf_snaps = self._get_sf_snapshots(sf_vol['volumeID']) + snap = next((s for s in sf_snaps if s["name"] == sf_snap_name), + None) + if snap: + params = {'snapshotID': snap['snapshotID']} + data = self._issue_api_request('DeleteSnapshot', + params, + version='6.0') + if 'result' not in data: + msg = (_("Failed to delete SolidFire Snapshot: %s") % + data) + raise exception.SolidFireAPIException(msg) + return + # Make sure it's not "old style" using clones as snaps + LOG.debug("Snapshot not found, checking old style clones.") + self.delete_volume(snapshot) def create_snapshot(self, snapshot): sfaccount = self._get_sfaccount(snapshot['project_id'])