1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2011 Justin Santa Barbara
6
# Licensed under the Apache License, Version 2.0 (the "License"); you may
7
# not use this file except in compliance with the License. You may obtain
8
# a copy of the License at
10
# http://www.apache.org/licenses/LICENSE-2.0
12
# Unless required by applicable law or agreed to in writing, software
13
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
# License for the specific language governing permissions and limitations
18
Drivers for san-stored volumes.
20
The unique thing about a SAN is that we don't expect that we can run the volume
21
controller on the SAN hardware. We expect to access it over SSH or some API.
32
from cinder import exception
33
from cinder import flags
34
from cinder.openstack.common import cfg
35
from cinder.openstack.common import log as logging
36
from cinder.volume.drivers.san.san import SanISCSIDriver
39
LOG = logging.getLogger(__name__)
42
cfg.BoolOpt('sf_emulate_512',
44
help='Set 512 byte emulation on volume creation; '),
48
help='IP address of SolidFire MVIP'),
50
cfg.StrOpt('sf_login',
52
help='Username for SF Cluster Admin'),
54
cfg.StrOpt('sf_password',
56
help='Password for SF Cluster Admin'),
58
cfg.BoolOpt('sf_allow_tenant_qos',
60
help='Allow tenants to specify QOS on create'), ]
63
FLAGS.register_opts(sf_opts)
66
class SolidFire(SanISCSIDriver):
68
sf_qos_dict = {'slow': {'minIOPS': 100,
71
'medium': {'minIOPS': 200,
74
'fast': {'minIOPS': 500,
77
'performant': {'minIOPS': 2000,
82
def __init__(self, *args, **kwargs):
83
super(SolidFire, self).__init__(*args, **kwargs)
85
def _issue_api_request(self, method_name, params):
86
"""All API requests to SolidFire device go through this method
88
Simple json-rpc web based API calls.
89
each call takes a set of paramaters (dict)
90
and returns results in a dict as well.
94
# For now 443 is the only port our server accepts requests on
97
# NOTE(john-griffith): Probably don't need this, but the idea is
98
# we provide a request_id so we can correlate
99
# responses with requests
100
request_id = int(uuid.uuid4()) # just generate a random number
102
cluster_admin = FLAGS.san_login
103
cluster_password = FLAGS.san_password
105
command = {'method': method_name,
108
if params is not None:
109
command['params'] = params
111
payload = json.dumps(command, ensure_ascii=False)
112
payload.encode('utf-8')
113
# we use json-rpc, webserver needs to see json-rpc in header
114
header = {'Content-Type': 'application/json-rpc; charset=utf-8'}
116
if cluster_password is not None:
117
# base64.encodestring includes a newline character
118
# in the result, make sure we strip it off
119
auth_key = base64.encodestring('%s:%s' % (cluster_admin,
120
cluster_password))[:-1]
121
header['Authorization'] = 'Basic %s' % auth_key
123
LOG.debug(_("Payload for SolidFire API call: %s"), payload)
124
connection = httplib.HTTPSConnection(host, port)
125
connection.request('POST', '/json-rpc/1.0', payload, header)
126
response = connection.getresponse()
129
if response.status != 200:
131
raise exception.SolidFireAPIException(status=response.status)
134
data = response.read()
136
data = json.loads(data)
138
except (TypeError, ValueError), exc:
140
msg = _("Call to json.loads() raised an exception: %s") % exc
141
raise exception.SfJsonEncodeFailure(msg)
145
LOG.debug(_("Results of SolidFire API call: %s"), data)
148
def _get_volumes_by_sfaccount(self, account_id):
149
params = {'accountID': account_id}
150
data = self._issue_api_request('ListVolumesForAccount', params)
152
return data['result']['volumes']
154
def _get_sfaccount_by_name(self, sf_account_name):
156
params = {'username': sf_account_name}
157
data = self._issue_api_request('GetAccountByName', params)
158
if 'result' in data and 'account' in data['result']:
159
LOG.debug(_('Found solidfire account: %s'), sf_account_name)
160
sfaccount = data['result']['account']
163
def _create_sfaccount(self, cinder_project_id):
164
"""Create account on SolidFire device if it doesn't already exist.
166
We're first going to check if the account already exits, if it does
167
just return it. If not, then create it.
170
sf_account_name = socket.gethostname() + '-' + cinder_project_id
171
sfaccount = self._get_sfaccount_by_name(sf_account_name)
172
if sfaccount is None:
173
LOG.debug(_('solidfire account: %s does not exist, create it...'),
175
chap_secret = self._generate_random_string(12)
176
params = {'username': sf_account_name,
177
'initiatorSecret': chap_secret,
178
'targetSecret': chap_secret,
180
data = self._issue_api_request('AddAccount', params)
182
sfaccount = self._get_sfaccount_by_name(sf_account_name)
186
def _get_cluster_info(self):
188
data = self._issue_api_request('GetClusterInfo', params)
189
if 'result' not in data:
190
raise exception.SolidFireAPIDataException(data=data)
192
return data['result']
194
def _do_export(self, volume):
195
"""Gets the associated account, retrieves CHAP info and updates."""
197
sfaccount_name = '%s-%s' % (socket.gethostname(), volume['project_id'])
198
sfaccount = self._get_sfaccount_by_name(sfaccount_name)
201
model_update['provider_auth'] = ('CHAP %s %s'
202
% (sfaccount['username'],
203
sfaccount['targetSecret']))
207
def _generate_random_string(self, length):
208
"""Generates random_string to use for CHAP password."""
210
char_set = string.ascii_uppercase + string.digits
211
return ''.join(random.sample(char_set, length))
213
def _do_volume_create(self, project_id, params):
214
cluster_info = self._get_cluster_info()
215
iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260'
216
sfaccount = self._create_sfaccount(project_id)
217
chap_secret = sfaccount['targetSecret']
219
params['accountID'] = sfaccount['accountID']
220
data = self._issue_api_request('CreateVolume', params)
222
if 'result' not in data or 'volumeID' not in data['result']:
223
raise exception.SolidFireAPIDataException(data=data)
225
volume_id = data['result']['volumeID']
227
volume_list = self._get_volumes_by_sfaccount(sfaccount['accountID'])
229
for v in volume_list:
230
if v['volumeID'] == volume_id:
236
# NOTE(john-griffith): SF volumes are always at lun 0
237
model_update['provider_location'] = ('%s %s %s'
238
% (iscsi_portal, iqn, 0))
239
model_update['provider_auth'] = ('CHAP %s %s'
240
% (sfaccount['username'],
245
def create_volume(self, volume):
246
"""Create volume on SolidFire device.
248
The account is where CHAP settings are derived from, volume is
249
created and exported. Note that the new volume is immediately ready
252
One caveat here is that an existing user account must be specified
253
in the API call to create a new volume. We use a set algorithm to
254
determine account info based on passed in cinder volume object. First
255
we check to see if the account already exists (and use it), or if it
256
does not already exist, we'll go ahead and create it.
258
For now, we're just using very basic settings, QOS is
259
turned off, 512 byte emulation is off etc. Will be
260
looking at extensions for these things later, or
261
this module can be hacked to suit needs.
267
qos_keys = ['minIOPS', 'maxIOPS', 'burstIOPS']
268
valid_presets = self.sf_qos_dict.keys()
270
if FLAGS.sf_allow_tenant_qos and \
271
volume.get('volume_metadata')is not None:
273
#First look to see if they included a preset
274
presets = [i.value for i in volume.get('volume_metadata')
275
if i.key == 'sf-qos' and i.value in valid_presets]
278
LOG.warning(_('More than one valid preset was '
279
'detected, using %s') % presets[0])
280
qos = self.sf_qos_dict[presets[0]]
282
#if there was no preset, look for explicit settings
283
for i in volume.get('volume_metadata'):
284
if i.key in qos_keys:
285
qos[i.key] = int(i.value)
287
params = {'name': 'OS-VOLID-%s' % volume['id'],
289
'sliceCount': slice_count,
290
'totalSize': volume['size'] * GB,
291
'enable512e': FLAGS.sf_emulate_512,
292
'attributes': attributes,
295
return self._do_volume_create(volume['project_id'], params)
297
def delete_volume(self, volume, is_snapshot=False):
298
"""Delete SolidFire Volume from device.
300
SolidFire allows multipe volumes with same name,
301
volumeID is what's guaranteed unique.
305
LOG.debug(_("Enter SolidFire delete_volume..."))
306
sf_account_name = socket.gethostname() + '-' + volume['project_id']
307
sfaccount = self._get_sfaccount_by_name(sf_account_name)
308
if sfaccount is None:
309
raise exception.SfAccountNotFound(account_name=sf_account_name)
311
params = {'accountID': sfaccount['accountID']}
312
data = self._issue_api_request('ListVolumesForAccount', params)
313
if 'result' not in data:
314
raise exception.SolidFireAPIDataException(data=data)
317
seek = 'OS-SNAPID-%s' % (volume['id'])
319
seek = 'OS-VOLID-%s' % volume['id']
320
#params = {'name': 'OS-VOLID-:%s' % volume['id'],
324
for v in data['result']['volumes']:
325
if v['name'] == seek:
327
volid = v['volumeID']
330
raise exception.VolumeNotFound(volume_id=volume['id'])
333
LOG.debug(_("Deleting volumeID: %s"), volid)
334
raise exception.DuplicateSfVolumeNames(vol_name=volume['id'])
336
params = {'volumeID': volid}
337
data = self._issue_api_request('DeleteVolume', params)
338
if 'result' not in data:
339
raise exception.SolidFireAPIDataException(data=data)
341
LOG.debug(_("Leaving SolidFire delete_volume"))
343
def ensure_export(self, context, volume):
344
LOG.debug(_("Executing SolidFire ensure_export..."))
345
return self._do_export(volume)
347
def create_export(self, context, volume):
348
LOG.debug(_("Executing SolidFire create_export..."))
349
return self._do_export(volume)
351
def _do_create_snapshot(self, snapshot, snapshot_name):
352
"""Creates a snapshot."""
353
LOG.debug(_("Enter SolidFire create_snapshot..."))
354
sf_account_name = socket.gethostname() + '-' + snapshot['project_id']
355
sfaccount = self._get_sfaccount_by_name(sf_account_name)
356
if sfaccount is None:
357
raise exception.SfAccountNotFound(account_name=sf_account_name)
359
params = {'accountID': sfaccount['accountID']}
360
data = self._issue_api_request('ListVolumesForAccount', params)
361
if 'result' not in data:
362
raise exception.SolidFireAPIDataException(data=data)
366
for v in data['result']['volumes']:
367
if v['name'] == 'OS-VOLID-%s' % snapshot['volume_id']:
369
volid = v['volumeID']
372
raise exception.VolumeNotFound(volume_id=snapshot['volume_id'])
374
raise exception.DuplicateSfVolumeNames(
375
vol_name='OS-VOLID-%s' % snapshot['volume_id'])
377
params = {'volumeID': int(volid),
378
'name': snapshot_name,
379
'attributes': {'OriginatingVolume': volid}}
381
data = self._issue_api_request('CloneVolume', params)
382
if 'result' not in data:
383
raise exception.SolidFireAPIDataException(data=data)
385
return (data, sfaccount)
387
def delete_snapshot(self, snapshot):
388
self.delete_volume(snapshot, True)
390
def create_snapshot(self, snapshot):
391
snapshot_name = 'OS-SNAPID-%s' % (
393
(data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name)
395
def create_volume_from_snapshot(self, volume, snapshot):
396
cluster_info = self._get_cluster_info()
397
iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260'
398
sfaccount = self._create_sfaccount(snapshot['project_id'])
399
chap_secret = sfaccount['targetSecret']
400
snapshot_name = 'OS-VOLID-%s' % volume['id']
402
(data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name)
404
if 'result' not in data or 'volumeID' not in data['result']:
405
raise exception.SolidFireAPIDataException(data=data)
407
volume_id = data['result']['volumeID']
408
volume_list = self._get_volumes_by_sfaccount(sf_account['accountID'])
410
for v in volume_list:
411
if v['volumeID'] == volume_id:
417
# NOTE(john-griffith): SF volumes are always at lun 0
418
model_update['provider_location'] = ('%s %s %s'
419
% (iscsi_portal, iqn, 0))
420
model_update['provider_auth'] = ('CHAP %s %s'
421
% (sfaccount['username'],