From: John Griffith Date: Tue, 14 Aug 2012 00:35:35 +0000 (-0600) Subject: Update SolidFire volume driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=df5c4ba864732d6534af561ebe2600d738c07f17;p=openstack-build%2Fcinder-build.git Update SolidFire volume driver Implements blueprint update-solidfire-driver * Updates driver to reflect changes in the release version of SF API * Modify SF naming scheme * Implement snapshot functionality * Implement setting qos on create via metadata * Update/Add tests Change-Id: I08f7aac31e9d95f971d297a19c285dfa7151b931 --- diff --git a/cinder/tests/test_SolidFireSanISCSIDriver.py b/cinder/tests/test_solidfire.py similarity index 54% rename from cinder/tests/test_SolidFireSanISCSIDriver.py rename to cinder/tests/test_solidfire.py index 0febe787b..d6d0772b6 100644 --- a/cinder/tests/test_SolidFireSanISCSIDriver.py +++ b/cinder/tests/test_solidfire.py @@ -17,8 +17,8 @@ from cinder import exception from cinder.openstack.common import log as logging -from cinder.volume import san from cinder import test +from cinder.volume.solidfire import SolidFire LOG = logging.getLogger(__name__) @@ -31,12 +31,12 @@ class SolidFireVolumeTestCase(test.TestCase): if method is 'GetClusterInfo': LOG.info('Called Fake GetClusterInfo...') results = {'result': {'clusterInfo': - {'name': 'fake-cluster', - 'mvip': '1.1.1.1', - 'svip': '1.1.1.1', - 'uniqueID': 'unqid', - 'repCount': 2, - 'attributes': {}}}} + {'name': 'fake-cluster', + 'mvip': '1.1.1.1', + 'svip': '1.1.1.1', + 'uniqueID': 'unqid', + 'repCount': 2, + 'attributes': {}}}} return results elif method is 'AddAccount': @@ -45,15 +45,15 @@ class SolidFireVolumeTestCase(test.TestCase): elif method is 'GetAccountByName': LOG.info('Called Fake GetAccountByName...') - results = {'result': {'account': { - 'accountID': 25, - 'username': params['username'], - 'status': 'active', - 'initiatorSecret': '123456789012', - 'targetSecret': '123456789012', - 'attributes': {}, - 'volumes': [6, 7, 20]}}, - "id": 1} + results = {'result': {'account': + {'accountID': 25, + 'username': params['username'], + 'status': 'active', + 'initiatorSecret': '123456789012', + 'targetSecret': '123456789012', + 'attributes': {}, + 'volumes': [6, 7, 20]}}, + "id": 1} return results elif method is 'CreateVolume': @@ -65,46 +65,67 @@ class SolidFireVolumeTestCase(test.TestCase): return {'result': {}, 'id': 1} elif method is 'ListVolumesForAccount': + test_name = 'OS-VOLID-a720b3c0-d1f0-11e1-9b23-0800200c9a66' LOG.info('Called Fake ListVolumesForAccount...') - result = {'result': {'volumes': [{ - 'volumeID': '5', - 'name': 'test_volume', - 'accountID': 25, - 'sliceCount': 1, - 'totalSize': 1048576 * 1024, - 'enable512e': False, - 'access': "readWrite", - 'status': "active", - 'attributes':None, - 'qos':None}]}} + result = {'result': { + 'volumes': [{'volumeID': 5, + 'name': test_name, + 'accountID': 25, + 'sliceCount': 1, + 'totalSize': 1048576 * 1024, + 'enable512e': True, + 'access': "readWrite", + 'status': "active", + 'attributes':None, + 'qos': None, + 'iqn': test_name}]}} return result else: LOG.error('Crap, unimplemented API call in Fake:%s' % method) def fake_issue_api_request_fails(obj, method, params): - return {'error': { - 'code': 000, - 'name': 'DummyError', - 'message': 'This is a fake error response'}, - 'id': 1} + return {'error': {'code': 000, + 'name': 'DummyError', + 'message': 'This is a fake error response'}, + 'id': 1} + + def fake_volume_get(obj, key, default=None): + return {'qos': 'fast'} def test_create_volume(self): - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + self.stubs.Set(SolidFire, '_issue_api_request', + self.fake_issue_api_request) + testvol = {'project_id': 'testprjid', + 'name': 'testvol', + 'size': 1, + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} + sfv = SolidFire() + model_update = sfv.create_volume(testvol) + + def test_create_volume_with_qos(self): + preset_qos = {} + preset_qos['qos'] = 'fast' + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request) + testvol = {'project_id': 'testprjid', 'name': 'testvol', - 'size': 1} - sfv = san.SolidFireSanISCSIDriver() + 'size': 1, + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', + 'metadata': [preset_qos]} + + sfv = SolidFire() model_update = sfv.create_volume(testvol) def test_create_volume_fails(self): - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request_fails) testvol = {'project_id': 'testprjid', 'name': 'testvol', - 'size': 1} - sfv = san.SolidFireSanISCSIDriver() + 'size': 1, + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} + sfv = SolidFire() try: sfv.create_volume(testvol) self.fail("Should have thrown Error") @@ -112,49 +133,51 @@ class SolidFireVolumeTestCase(test.TestCase): pass def test_create_sfaccount(self): - sfv = san.SolidFireSanISCSIDriver() - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + sfv = SolidFire() + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request) account = sfv._create_sfaccount('project-id') self.assertNotEqual(account, None) def test_create_sfaccount_fails(self): - sfv = san.SolidFireSanISCSIDriver() - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + sfv = SolidFire() + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request_fails) account = sfv._create_sfaccount('project-id') self.assertEqual(account, None) def test_get_sfaccount_by_name(self): - sfv = san.SolidFireSanISCSIDriver() - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + sfv = SolidFire() + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request) account = sfv._get_sfaccount_by_name('some-name') self.assertNotEqual(account, None) def test_get_sfaccount_by_name_fails(self): - sfv = san.SolidFireSanISCSIDriver() - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + sfv = SolidFire() + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request_fails) account = sfv._get_sfaccount_by_name('some-name') self.assertEqual(account, None) def test_delete_volume(self): - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request) testvol = {'project_id': 'testprjid', 'name': 'test_volume', - 'size': 1} - sfv = san.SolidFireSanISCSIDriver() + 'size': 1, + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} + sfv = SolidFire() model_update = sfv.delete_volume(testvol) def test_delete_volume_fails_no_volume(self): - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request) testvol = {'project_id': 'testprjid', 'name': 'no-name', - 'size': 1} - sfv = san.SolidFireSanISCSIDriver() + 'size': 1, + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} + sfv = SolidFire() try: model_update = sfv.delete_volume(testvol) self.fail("Should have thrown Error") @@ -162,25 +185,26 @@ class SolidFireVolumeTestCase(test.TestCase): pass def test_delete_volume_fails_account_lookup(self): - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', - self.fake_issue_api_request) + self.stubs.Set(SolidFire, '_issue_api_request', + self.fake_issue_api_request_fails) testvol = {'project_id': 'testprjid', 'name': 'no-name', - 'size': 1} - sfv = san.SolidFireSanISCSIDriver() - self.assertRaises(exception.DuplicateSfVolumeNames, + 'size': 1, + 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} + sfv = SolidFire() + self.assertRaises(exception.SfAccountNotFound, sfv.delete_volume, testvol) def test_get_cluster_info(self): - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request) - sfv = san.SolidFireSanISCSIDriver() + sfv = SolidFire() sfv._get_cluster_info() def test_get_cluster_info_fail(self): - self.stubs.Set(san.SolidFireSanISCSIDriver, '_issue_api_request', + self.stubs.Set(SolidFire, '_issue_api_request', self.fake_issue_api_request_fails) - sfv = san.SolidFireSanISCSIDriver() + sfv = SolidFire() self.assertRaises(exception.SolidFireAPIException, sfv._get_cluster_info) diff --git a/cinder/volume/san.py b/cinder/volume/san.py index 0448f8092..b70c71fa3 100644 --- a/cinder/volume/san.py +++ b/cinder/volume/san.py @@ -644,246 +644,3 @@ class HpSanISCSIDriver(SanISCSIDriver): cliq_args['volumeName'] = volume['name'] cliq_args['serverName'] = connector['host'] self._cliq_run_xml("unassignVolumeToServer", cliq_args) - - -class SolidFireSanISCSIDriver(SanISCSIDriver): - - def _issue_api_request(self, method_name, params): - """All API requests to SolidFire device go through this method - - Simple json-rpc web based API calls. - each call takes a set of paramaters (dict) - and returns results in a dict as well. - """ - - host = FLAGS.san_ip - # For now 443 is the only port our server accepts requests on - port = 443 - - # NOTE(john-griffith): Probably don't need this, but the idea is - # we provide a request_id so we can correlate - # responses with requests - request_id = int(uuid.uuid4()) # just generate a random number - - cluster_admin = FLAGS.san_login - cluster_password = FLAGS.san_password - - command = {'method': method_name, - 'id': request_id} - - if params is not None: - command['params'] = params - - payload = jsonutils.dumps(command, ensure_ascii=False) - payload.encode('utf-8') - # we use json-rpc, webserver needs to see json-rpc in header - header = {'Content-Type': 'application/json-rpc; charset=utf-8'} - - if cluster_password is not None: - # base64.encodestring includes a newline character - # in the result, make sure we strip it off - auth_key = base64.encodestring('%s:%s' % (cluster_admin, - cluster_password))[:-1] - header['Authorization'] = 'Basic %s' % auth_key - - LOG.debug(_("Payload for SolidFire API call: %s"), payload) - connection = httplib.HTTPSConnection(host, port) - connection.request('POST', '/json-rpc/1.0', payload, header) - response = connection.getresponse() - data = {} - - if response.status != 200: - connection.close() - raise exception.SolidFireAPIException(status=response.status) - - else: - data = response.read() - try: - data = jsonutils.loads(data) - - except (TypeError, ValueError), exc: - connection.close() - msg = _("Call to json.loads() raised an exception: %s") % exc - raise exception.SfJsonEncodeFailure(msg) - - connection.close() - - LOG.debug(_("Results of SolidFire API call: %s"), data) - return data - - def _get_volumes_by_sfaccount(self, account_id): - params = {'accountID': account_id} - data = self._issue_api_request('ListVolumesForAccount', params) - if 'result' in data: - return data['result']['volumes'] - - def _get_sfaccount_by_name(self, sf_account_name): - sfaccount = None - params = {'username': sf_account_name} - data = self._issue_api_request('GetAccountByName', params) - if 'result' in data and 'account' in data['result']: - LOG.debug(_('Found solidfire account: %s'), sf_account_name) - sfaccount = data['result']['account'] - return sfaccount - - def _create_sfaccount(self, cinder_project_id): - """Create account on SolidFire device if it doesn't already exist. - - We're first going to check if the account already exits, if it does - just return it. If not, then create it. - """ - - sf_account_name = socket.gethostname() + '-' + cinder_project_id - sfaccount = self._get_sfaccount_by_name(sf_account_name) - if sfaccount is None: - LOG.debug(_('solidfire account: %s does not exist, create it...'), - sf_account_name) - chap_secret = self._generate_random_string(12) - params = {'username': sf_account_name, - 'initiatorSecret': chap_secret, - 'targetSecret': chap_secret, - 'attributes': {}} - data = self._issue_api_request('AddAccount', params) - if 'result' in data: - sfaccount = self._get_sfaccount_by_name(sf_account_name) - - return sfaccount - - def _get_cluster_info(self): - params = {} - data = self._issue_api_request('GetClusterInfo', params) - if 'result' not in data: - raise exception.SolidFireAPIDataException(data=data) - - return data['result'] - - def _do_export(self, volume): - """Gets the associated account, retrieves CHAP info and updates.""" - - sfaccount_name = '%s-%s' % (socket.gethostname(), volume['project_id']) - sfaccount = self._get_sfaccount_by_name(sfaccount_name) - - model_update = {} - model_update['provider_auth'] = ('CHAP %s %s' - % (sfaccount['username'], sfaccount['targetSecret'])) - - return model_update - - def _generate_random_string(self, length): - """Generates random_string to use for CHAP password.""" - - char_set = string.ascii_uppercase + string.digits - return ''.join(random.sample(char_set, length)) - - def create_volume(self, volume): - """Create volume on SolidFire device. - - The account is where CHAP settings are derived from, volume is - created and exported. Note that the new volume is immediately ready - for use. - - One caveat here is that an existing user account must be specified - in the API call to create a new volume. We use a set algorithm to - determine account info based on passed in cinder volume object. First - we check to see if the account already exists (and use it), or if it - does not already exist, we'll go ahead and create it. - - For now, we're just using very basic settings, QOS is - turned off, 512 byte emulation is off etc. Will be - looking at extensions for these things later, or - this module can be hacked to suit needs. - """ - - LOG.debug(_("Enter SolidFire create_volume...")) - GB = 1048576 * 1024 - slice_count = 1 - enable_emulation = False - attributes = {} - - cluster_info = self._get_cluster_info() - iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260' - sfaccount = self._create_sfaccount(volume['project_id']) - account_id = sfaccount['accountID'] - account_name = sfaccount['username'] - chap_secret = sfaccount['targetSecret'] - - params = {'name': volume['name'], - 'accountID': account_id, - 'sliceCount': slice_count, - 'totalSize': volume['size'] * GB, - 'enable512e': enable_emulation, - 'attributes': attributes} - - data = self._issue_api_request('CreateVolume', params) - - if 'result' not in data or 'volumeID' not in data['result']: - raise exception.SolidFireAPIDataException(data=data) - - volume_id = data['result']['volumeID'] - - volume_list = self._get_volumes_by_sfaccount(account_id) - iqn = None - for v in volume_list: - if v['volumeID'] == volume_id: - iqn = 'iqn.2010-01.com.solidfire:' + v['iqn'] - break - - model_update = {} - - # NOTE(john-griffith): SF volumes are always at lun 0 - model_update['provider_location'] = ('%s %s %s' - % (iscsi_portal, iqn, 0)) - model_update['provider_auth'] = ('CHAP %s %s' - % (account_name, chap_secret)) - - LOG.debug(_("Leaving SolidFire create_volume")) - return model_update - - def delete_volume(self, volume): - """Delete SolidFire Volume from device. - - SolidFire allows multipe volumes with same name, - volumeID is what's guaranteed unique. - - What we'll do here is check volumes based on account. this - should work because cinder will increment its volume_id - so we should always get the correct volume. This assumes - that cinder does not assign duplicate ID's. - """ - - LOG.debug(_("Enter SolidFire delete_volume...")) - sf_account_name = socket.gethostname() + '-' + volume['project_id'] - sfaccount = self._get_sfaccount_by_name(sf_account_name) - if sfaccount is None: - raise exception.SfAccountNotFound(account_name=sf_account_name) - - params = {'accountID': sfaccount['accountID']} - data = self._issue_api_request('ListVolumesForAccount', params) - if 'result' not in data: - raise exception.SolidFireAPIDataException(data=data) - - found_count = 0 - volid = -1 - for v in data['result']['volumes']: - if v['name'] == volume['name']: - found_count += 1 - volid = v['volumeID'] - - if found_count != 1: - LOG.debug(_("Deleting volumeID: %s"), volid) - raise exception.DuplicateSfVolumeNames(vol_name=volume['name']) - - params = {'volumeID': volid} - data = self._issue_api_request('DeleteVolume', params) - if 'result' not in data: - raise exception.SolidFireAPIDataException(data=data) - - LOG.debug(_("Leaving SolidFire delete_volume")) - - def ensure_export(self, context, volume): - LOG.debug(_("Executing SolidFire ensure_export...")) - return self._do_export(volume) - - def create_export(self, context, volume): - LOG.debug(_("Executing SolidFire create_export...")) - return self._do_export(volume) diff --git a/cinder/volume/solidfire.py b/cinder/volume/solidfire.py new file mode 100644 index 000000000..c042f5e67 --- /dev/null +++ b/cinder/volume/solidfire.py @@ -0,0 +1,423 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +Drivers for san-stored volumes. + +The unique thing about a SAN is that we don't expect that we can run the volume +controller on the SAN hardware. We expect to access it over SSH or some API. +""" + +import base64 +import httplib +import json +import random +import socket +import string +import uuid + +from cinder import exception +from cinder import flags +from cinder.openstack.common import cfg +from cinder.openstack.common import log as logging +from cinder.volume.san import SanISCSIDriver + + +LOG = logging.getLogger(__name__) + +sf_opts = [ + cfg.BoolOpt('sf_emulate_512', + default=True, + help='Set 512 byte emulation on volume creation; '), + + cfg.StrOpt('sf_mvip', + default='', + help='IP address of SolidFire MVIP'), + + cfg.StrOpt('sf_login', + default='admin', + help='Username for SF Cluster Admin'), + + cfg.StrOpt('sf_password', + default='', + help='Password for SF Cluster Admin'), + + cfg.StrOpt('sf_allow_tenant_qos', + default=True, + help='Allow tenants to specify QOS on create'), ] + +FLAGS = flags.FLAGS +FLAGS.register_opts(sf_opts) + + +class SolidFire(SanISCSIDriver): + + sf_qos_dict = {'slow': {'minIOPS': 100, + 'maxIOPS': 200, + 'burstIOPS': 200}, + 'medium': {'minIOPS': 200, + 'maxIOPS': 400, + 'burstIOPS': 400}, + 'fast': {'minIOPS': 500, + 'maxIOPS': 1000, + 'burstIOPS': 1000}, + 'performant': {'minIOPS': 2000, + 'maxIOPS': 4000, + 'burstIOPS': 4000}, + 'off': None} + + def __init__(self, *args, **kwargs): + super(SolidFire, self).__init__(*args, **kwargs) + + def _issue_api_request(self, method_name, params): + """All API requests to SolidFire device go through this method + + Simple json-rpc web based API calls. + each call takes a set of paramaters (dict) + and returns results in a dict as well. + """ + + host = FLAGS.san_ip + # For now 443 is the only port our server accepts requests on + port = 443 + + # NOTE(john-griffith): Probably don't need this, but the idea is + # we provide a request_id so we can correlate + # responses with requests + request_id = int(uuid.uuid4()) # just generate a random number + + cluster_admin = FLAGS.san_login + cluster_password = FLAGS.san_password + + command = {'method': method_name, + 'id': request_id} + + if params is not None: + command['params'] = params + + payload = json.dumps(command, ensure_ascii=False) + payload.encode('utf-8') + # we use json-rpc, webserver needs to see json-rpc in header + header = {'Content-Type': 'application/json-rpc; charset=utf-8'} + + if cluster_password is not None: + # base64.encodestring includes a newline character + # in the result, make sure we strip it off + auth_key = base64.encodestring('%s:%s' % (cluster_admin, + cluster_password))[:-1] + header['Authorization'] = 'Basic %s' % auth_key + + LOG.debug(_("Payload for SolidFire API call: %s"), payload) + connection = httplib.HTTPSConnection(host, port) + connection.request('POST', '/json-rpc/1.0', payload, header) + response = connection.getresponse() + data = {} + + if response.status != 200: + connection.close() + raise exception.SolidFireAPIException(status=response.status) + + else: + data = response.read() + try: + data = json.loads(data) + + except (TypeError, ValueError), exc: + connection.close() + msg = _("Call to json.loads() raised an exception: %s") % exc + raise exception.SfJsonEncodeFailure(msg) + + connection.close() + + LOG.debug(_("Results of SolidFire API call: %s"), data) + return data + + def _get_volumes_by_sfaccount(self, account_id): + params = {'accountID': account_id} + data = self._issue_api_request('ListVolumesForAccount', params) + if 'result' in data: + return data['result']['volumes'] + + def _get_sfaccount_by_name(self, sf_account_name): + sfaccount = None + params = {'username': sf_account_name} + data = self._issue_api_request('GetAccountByName', params) + if 'result' in data and 'account' in data['result']: + LOG.debug(_('Found solidfire account: %s'), sf_account_name) + sfaccount = data['result']['account'] + return sfaccount + + def _create_sfaccount(self, cinder_project_id): + """Create account on SolidFire device if it doesn't already exist. + + We're first going to check if the account already exits, if it does + just return it. If not, then create it. + """ + + sf_account_name = socket.gethostname() + '-' + cinder_project_id + sfaccount = self._get_sfaccount_by_name(sf_account_name) + if sfaccount is None: + LOG.debug(_('solidfire account: %s does not exist, create it...'), + sf_account_name) + chap_secret = self._generate_random_string(12) + params = {'username': sf_account_name, + 'initiatorSecret': chap_secret, + 'targetSecret': chap_secret, + 'attributes': {}} + data = self._issue_api_request('AddAccount', params) + if 'result' in data: + sfaccount = self._get_sfaccount_by_name(sf_account_name) + + return sfaccount + + def _get_cluster_info(self): + params = {} + data = self._issue_api_request('GetClusterInfo', params) + if 'result' not in data: + raise exception.SolidFireAPIDataException(data=data) + + return data['result'] + + def _do_export(self, volume): + """Gets the associated account, retrieves CHAP info and updates.""" + + sfaccount_name = '%s-%s' % (socket.gethostname(), volume['project_id']) + sfaccount = self._get_sfaccount_by_name(sfaccount_name) + + model_update = {} + model_update['provider_auth'] = ('CHAP %s %s' + % (sfaccount['username'], + sfaccount['targetSecret'])) + + return model_update + + def _generate_random_string(self, length): + """Generates random_string to use for CHAP password.""" + + char_set = string.ascii_uppercase + string.digits + return ''.join(random.sample(char_set, length)) + + def _do_volume_create(self, project_id, params): + cluster_info = self._get_cluster_info() + iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260' + sfaccount = self._create_sfaccount(project_id) + chap_secret = sfaccount['targetSecret'] + + params['accountID'] = sfaccount['accountID'] + data = self._issue_api_request('CreateVolume', params) + + if 'result' not in data or 'volumeID' not in data['result']: + raise exception.SolidFireAPIDataException(data=data) + + volume_id = data['result']['volumeID'] + + volume_list = self._get_volumes_by_sfaccount(sfaccount['accountID']) + iqn = None + for v in volume_list: + if v['volumeID'] == volume_id: + iqn = 'iqn.2010-01.com.solidfire:' + v['iqn'] + break + + model_update = {} + + # NOTE(john-griffith): SF volumes are always at lun 0 + model_update['provider_location'] = ('%s %s %s' + % (iscsi_portal, iqn, 0)) + model_update['provider_auth'] = ('CHAP %s %s' + % (sfaccount['username'], + chap_secret)) + + return model_update + + def create_volume(self, volume): + """Create volume on SolidFire device. + + The account is where CHAP settings are derived from, volume is + created and exported. Note that the new volume is immediately ready + for use. + + One caveat here is that an existing user account must be specified + in the API call to create a new volume. We use a set algorithm to + determine account info based on passed in cinder volume object. First + we check to see if the account already exists (and use it), or if it + does not already exist, we'll go ahead and create it. + + For now, we're just using very basic settings, QOS is + turned off, 512 byte emulation is off etc. Will be + looking at extensions for these things later, or + this module can be hacked to suit needs. + """ + GB = 1048576 * 1024 + slice_count = 1 + attributes = {} + qos = {} + qos_keys = ['minIOPS', 'maxIOPS', 'burstIOPS'] + valid_presets = self.sf_qos_dict.keys() + + if FLAGS.sf_allow_tenant_qos and \ + volume.get('volume_metadata')is not None: + + #First look to see if they included a preset + presets = [i.value for i in volume.get('volume_metadata') + if i.key == 'sf-qos' and i.value in valid_presets] + if len(presets) > 0: + if len(presets) > 1: + LOG.warning(_('More than one valid preset was ' + 'detected, using %s' % presets[0])) + qos = self.sf_qos_dict[presets[0]] + else: + #if there was no preset, look for explicit settings + for i in volume.get('volume_metadata'): + if i.key in qos_keys: + qos[i.key] = int(i.value) + + params = {'name': 'OS-VOLID-%s' % volume['id'], + 'accountID': None, + 'sliceCount': slice_count, + 'totalSize': volume['size'] * GB, + 'enable512e': FLAGS.sf_emulate_512, + 'attributes': attributes, + 'qos': qos} + + return self._do_volume_create(volume['project_id'], params) + + def delete_volume(self, volume, is_snapshot=False): + """Delete SolidFire Volume from device. + + SolidFire allows multipe volumes with same name, + volumeID is what's guaranteed unique. + + """ + + LOG.debug(_("Enter SolidFire delete_volume...")) + sf_account_name = socket.gethostname() + '-' + volume['project_id'] + sfaccount = self._get_sfaccount_by_name(sf_account_name) + if sfaccount is None: + raise exception.SfAccountNotFound(account_name=sf_account_name) + + params = {'accountID': sfaccount['accountID']} + data = self._issue_api_request('ListVolumesForAccount', params) + if 'result' not in data: + raise exception.SolidFireAPIDataException(data=data) + + if is_snapshot: + seek = 'OS-SNAPID-%s' % (volume['id']) + else: + seek = 'OS-VOLID-%s' % volume['id'] + #params = {'name': 'OS-VOLID-:%s' % volume['id'], + + found_count = 0 + volid = -1 + for v in data['result']['volumes']: + if v['name'] == seek: + found_count += 1 + volid = v['volumeID'] + + if found_count == 0: + raise exception.VolumeNotFound(volume_id=volume['id']) + + if found_count > 1: + LOG.debug(_("Deleting volumeID: %s"), volid) + raise exception.DuplicateSfVolumeNames(vol_name=volume['id']) + + params = {'volumeID': volid} + data = self._issue_api_request('DeleteVolume', params) + if 'result' not in data: + raise exception.SolidFireAPIDataException(data=data) + + LOG.debug(_("Leaving SolidFire delete_volume")) + + def ensure_export(self, context, volume): + LOG.debug(_("Executing SolidFire ensure_export...")) + return self._do_export(volume) + + def create_export(self, context, volume): + LOG.debug(_("Executing SolidFire create_export...")) + return self._do_export(volume) + + def _do_create_snapshot(self, snapshot, snapshot_name): + """Creates a snapshot.""" + LOG.debug(_("Enter SolidFire create_snapshot...")) + sf_account_name = socket.gethostname() + '-' + snapshot['project_id'] + sfaccount = self._get_sfaccount_by_name(sf_account_name) + if sfaccount is None: + raise exception.SfAccountNotFound(account_name=sf_account_name) + + params = {'accountID': sfaccount['accountID']} + data = self._issue_api_request('ListVolumesForAccount', params) + if 'result' not in data: + raise exception.SolidFireAPIDataException(data=data) + + found_count = 0 + volid = -1 + for v in data['result']['volumes']: + if v['name'] == 'OS-VOLID-%s' % snapshot['volume_id']: + found_count += 1 + volid = v['volumeID'] + + if found_count == 0: + raise exception.VolumeNotFound(volume_id=snapshot['volume_id']) + if found_count != 1: + raise exception.DuplicateSfVolumeNames( + vol_name='OS-VOLID-%s' % snapshot['volume_id']) + + params = {'volumeID': int(volid), + 'name': snapshot_name, + 'attributes': {'OriginatingVolume': volid}} + + data = self._issue_api_request('CloneVolume', params) + if 'result' not in data: + raise exception.SolidFireAPIDataException(data=data) + + return (data, sfaccount) + + def delete_snapshot(self, snapshot): + self.delete_volume(snapshot, True) + + def create_snapshot(self, snapshot): + snapshot_name = 'OS-SNAPID-%s' % ( + snapshot['id']) + (data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name) + + def create_volume_from_snapshot(self, volume, snapshot): + cluster_info = self._get_cluster_info() + iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260' + sfaccount = self._create_sfaccount(snapshot['project_id']) + chap_secret = sfaccount['targetSecret'] + snapshot_name = 'OS-VOLID-%s' % volume['id'] + + (data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name) + + if 'result' not in data or 'volumeID' not in data['result']: + raise exception.SolidFireAPIDataException(data=data) + + volume_id = data['result']['volumeID'] + volume_list = self._get_volumes_by_sfaccount(sf_account['accountID']) + iqn = None + for v in volume_list: + if v['volumeID'] == volume_id: + iqn = 'iqn.2010-01.com.solidfire:' + v['iqn'] + break + + model_update = {} + + # NOTE(john-griffith): SF volumes are always at lun 0 + model_update['provider_location'] = ('%s %s %s' + % (iscsi_portal, iqn, 0)) + model_update['provider_auth'] = ('CHAP %s %s' + % (sfaccount['username'], + chap_secret)) + return model_update