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
This commit is contained in:
parent
87ba5de73e
commit
df5c4ba864
@ -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}
|
||||
sfv = san.SolidFireSanISCSIDriver()
|
||||
'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,
|
||||
'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)
|
@ -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)
|
||||
|
423
cinder/volume/solidfire.py
Normal file
423
cinder/volume/solidfire.py
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user