79
73
'burstIOPS': 4000},
76
sf_qos_keys = ['minIOPS', 'maxIOPS', 'burstIOPS']
82
81
def __init__(self, *args, **kwargs):
83
82
super(SolidFire, self).__init__(*args, **kwargs)
83
self._update_cluster_status()
85
85
def _issue_api_request(self, method_name, params):
86
"""All API requests to SolidFire device go through this method
86
"""All API requests to SolidFire device go through this method.
88
88
Simple json-rpc web based API calls.
89
89
each call takes a set of paramaters (dict)
90
90
and returns results in a dict as well.
93
max_simultaneous_clones = ['xMaxSnapshotsPerVolumeExceeded',
94
'xMaxClonesPerVolumeExceeded',
95
'xMaxSnapshotsPerNodeExceeded',
96
'xMaxClonesPerNodeExceeded']
93
97
host = FLAGS.san_ip
94
98
# 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
101
cluster_admin = FLAGS.san_login
103
102
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)
104
# NOTE(jdg): We're wrapping a retry loop for a know XDB issue
105
# Shows up in very high request rates (ie create 1000 volumes)
106
# we have to wrap the whole sequence because the request_id
109
while retry_count > 0:
110
request_id = int(uuid.uuid4()) # just generate a random number
111
command = {'method': method_name,
114
if params is not None:
115
command['params'] = params
117
payload = json.dumps(command, ensure_ascii=False)
118
payload.encode('utf-8')
119
header = {'Content-Type': 'application/json-rpc; charset=utf-8'}
121
if cluster_password is not None:
122
# base64.encodestring includes a newline character
123
# in the result, make sure we strip it off
124
auth_key = base64.encodestring('%s:%s' % (cluster_admin,
125
cluster_password))[:-1]
126
header['Authorization'] = 'Basic %s' % auth_key
128
LOG.debug(_("Payload for SolidFire API call: %s"), payload)
130
connection = httplib.HTTPSConnection(host, port)
131
connection.request('POST', '/json-rpc/1.0', payload, header)
132
response = connection.getresponse()
135
if response.status != 200:
137
raise exception.SolidFireAPIException(status=response.status)
140
data = response.read()
142
data = json.loads(data)
143
except (TypeError, ValueError), exc:
145
msg = _("Call to json.loads() raised "
146
"an exception: %s") % exc
147
raise exception.SfJsonEncodeFailure(msg)
151
LOG.debug(_("Results of SolidFire API call: %s"), data)
154
if data['error']['name'] in max_simultaneous_clones:
155
LOG.warning(_('Clone operation '
156
'encountered: %s') % data['error']['name'])
158
'Waiting for outstanding operation '
159
'before retrying snapshot: %s') % params['name'])
161
# Don't decrement the retry count for this one
162
elif 'xDBVersionMismatch' in data['error']['name']:
163
LOG.debug(_('Detected xDBVersionMismatch, '
164
'retry %s of 5') % (5 - retry_count))
168
msg = _("API response: %s") % data
169
raise exception.SolidFireAPIException(msg)
148
175
def _get_volumes_by_sfaccount(self, account_id):
176
"""Get all volumes on cluster for specified account."""
149
177
params = {'accountID': account_id}
150
178
data = self._issue_api_request('ListVolumesForAccount', params)
151
179
if 'result' in data:
152
180
return data['result']['volumes']
154
182
def _get_sfaccount_by_name(self, sf_account_name):
183
"""Get SolidFire account object by name."""
156
185
params = {'username': sf_account_name}
157
186
data = self._issue_api_request('GetAccountByName', params)
210
251
char_set = string.ascii_uppercase + string.digits
211
252
return ''.join(random.sample(char_set, length))
254
def _get_model_info(self, sfaccount, sf_volume_id):
255
"""Gets the connection info for specified account and volume."""
256
cluster_info = self._get_cluster_info()
257
iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260'
258
chap_secret = sfaccount['targetSecret']
260
volume_list = self._get_volumes_by_sfaccount(sfaccount['accountID'])
262
for v in volume_list:
263
if v['volumeID'] == sf_volume_id:
268
# NOTE(john-griffith): SF volumes are always at lun 0
269
model_update['provider_location'] = ('%s %s %s'
270
% (iscsi_portal, iqn, 0))
271
model_update['provider_auth'] = ('CHAP %s %s'
272
% (sfaccount['username'],
276
def _do_clone_volume(self, src_uuid, src_project_id, v_ref):
277
"""Create a clone of an existing volume.
279
Currently snapshots are the same as clones on the SF cluster.
280
Due to the way the SF cluster works there's no loss in efficiency
281
or space usage between the two. The only thing different right
282
now is the restore snapshot functionality which has not been
283
implemented in the pre-release version of the SolidFire Cluster.
289
sfaccount = self._get_sfaccount(src_project_id)
290
params = {'accountID': sfaccount['accountID']}
292
sf_vol = self._get_sf_volume(src_uuid, params)
294
raise exception.VolumeNotFound(volume_id=uuid)
299
attributes = {'uuid': v_ref['id'],
301
'src_uuid': 'src_uuid'}
304
attributes['qos'] = qos
306
params = {'volumeID': int(sf_vol['volumeID']),
307
'name': 'UUID-%s' % v_ref['id'],
308
'attributes': attributes,
311
data = self._issue_api_request('CloneVolume', params)
313
if (('result' not in data) or ('volumeID' not in data['result'])):
314
raise exception.SolidFireAPIDataException(data=data)
316
sf_volume_id = data['result']['volumeID']
317
model_update = self._get_model_info(sfaccount, sf_volume_id)
318
if model_update is None:
319
mesg = _('Failed to get model update from clone')
320
raise exception.SolidFireAPIDataException(mesg)
322
return (data, sfaccount, model_update)
213
324
def _do_volume_create(self, project_id, params):
214
cluster_info = self._get_cluster_info()
215
iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260'
216
325
sfaccount = self._create_sfaccount(project_id)
217
chap_secret = sfaccount['targetSecret']
219
327
params['accountID'] = sfaccount['accountID']
220
328
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'],
330
if (('result' not in data) or ('volumeID' not in data['result'])):
331
raise exception.SolidFireAPIDataException(data=data)
333
sf_volume_id = data['result']['volumeID']
334
return self._get_model_info(sfaccount, sf_volume_id)
336
def _set_qos_presets(self, volume):
338
valid_presets = self.sf_qos_dict.keys()
340
#First look to see if they included a preset
341
presets = [i.value for i in volume.get('volume_metadata')
342
if i.key == 'sf-qos' and i.value in valid_presets]
345
LOG.warning(_('More than one valid preset was '
346
'detected, using %s') % presets[0])
347
qos = self.sf_qos_dict[presets[0]]
349
#look for explicit settings
350
for i in volume.get('volume_metadata'):
351
if i.key in self.sf_qos_keys:
352
qos[i.key] = int(i.value)
355
def _set_qos_by_volume_type(self, type_id, ctxt):
357
volume_type = volume_types.get_volume_type(ctxt, type_id)
358
specs = volume_type.get('extra_specs')
359
for key, value in specs.iteritems():
360
if key in self.sf_qos_keys:
361
qos[key] = int(value)
364
def _get_sf_volume(self, uuid, params):
365
data = self._issue_api_request('ListVolumesForAccount', params)
366
if 'result' not in data:
367
raise exception.SolidFireAPIDataException(data=data)
371
for v in data['result']['volumes']:
372
if uuid in v['name']:
375
LOG.debug(_("Mapped SolidFire volumeID %(sfid)s "
376
"to cinder ID %(uuid)s.") %
377
{'sfid': v['volumeID'],
381
# NOTE(jdg): Previously we would raise here, but there are cases
382
# where this might be a cleanup for a failed delete.
383
# Until we get better states we'll just log an error
384
LOG.error(_("Volume %s, not found on SF Cluster."), uuid)
387
LOG.error(_("Found %(count)s volumes mapped to id: %(uuid)s.") %
388
{'count': found_count,
390
raise exception.DuplicateSfVolumeNames(vol_name=uuid)
245
394
def create_volume(self, volume):
246
395
"""Create volume on SolidFire device.
255
404
we check to see if the account already exists (and use it), or if it
256
405
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'],
412
if (FLAGS.sf_allow_tenant_qos and
413
volume.get('volume_metadata')is not None):
414
qos = self._set_qos_presets(volume)
416
ctxt = context.get_admin_context()
417
type_id = volume['volume_type_id']
418
if type_id is not None:
419
qos = self._set_qos_by_volume_type(ctxt, type_id)
421
attributes = {'uuid': volume['id'],
424
attributes['qos'] = qos
426
params = {'name': 'UUID-%s' % volume['id'],
288
427
'accountID': None,
289
428
'sliceCount': slice_count,
290
'totalSize': volume['size'] * GB,
429
'totalSize': int(volume['size'] * self.GB),
291
430
'enable512e': FLAGS.sf_emulate_512,
292
431
'attributes': attributes,
295
434
return self._do_volume_create(volume['project_id'], params)
297
def delete_volume(self, volume, is_snapshot=False):
436
def create_cloned_volume(self, volume, src_vref):
437
"""Create a clone of an existing volume."""
438
(data, sfaccount, model) = self._do_clone_volume(
440
src_vref['project_id'],
445
def delete_volume(self, volume):
298
446
"""Delete SolidFire Volume from device.
300
448
SolidFire allows multipe volumes with same name,
305
453
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)
455
sfaccount = self._get_sfaccount(volume['project_id'])
311
456
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'])
458
sf_vol = self._get_sf_volume(volume['id'], params)
460
if sf_vol is not None:
461
params = {'volumeID': sf_vol['volumeID']}
462
data = self._issue_api_request('DeleteVolume', params)
464
if 'result' not in data:
465
raise exception.SolidFireAPIDataException(data=data)
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)
467
LOG.error(_("Volume ID %s was not found on "
468
"the SolidFire Cluster!"), volume['id'])
341
470
LOG.debug(_("Leaving SolidFire delete_volume"))
343
472
def ensure_export(self, context, volume):
473
"""Verify the iscsi export info."""
344
474
LOG.debug(_("Executing SolidFire ensure_export..."))
345
475
return self._do_export(volume)
347
477
def create_export(self, context, volume):
478
"""Setup the iscsi export info."""
348
479
LOG.debug(_("Executing SolidFire create_export..."))
349
480
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
482
def delete_snapshot(self, snapshot):
388
self.delete_volume(snapshot, True)
483
"""Delete the specified snapshot from the SolidFire cluster."""
484
self.delete_volume(snapshot)
390
486
def create_snapshot(self, snapshot):
391
snapshot_name = 'OS-SNAPID-%s' % (
393
(data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name)
487
"""Create a snapshot of a volume on the SolidFire cluster.
489
Note that for SolidFire Clusters currently there is no snapshot
490
implementation. Due to the way SF does cloning there's no performance
491
hit or extra space used. The only thing that's lacking from this is
492
the abilit to restore snaps.
494
After GA a true snapshot implementation will be available with
495
restore at which time we'll rework this appropriately.
498
(data, sfaccount, model) = self._do_clone_volume(
499
snapshot['volume_id'],
500
snapshot['project_id'],
395
503
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'],
504
"""Create a volume from the specified snapshot."""
505
(data, sfaccount, model) = self._do_clone_volume(
507
snapshot['project_id'],
512
def get_volume_stats(self, refresh=False):
513
"""Get volume status.
515
If 'refresh' is True, run update first.
516
The name is a bit misleading as
517
the majority of the data here is cluster
521
self._update_cluster_status()
523
return self.cluster_stats
525
def _update_cluster_status(self):
526
"""Retrieve status info for the Cluster."""
528
LOG.debug(_("Updating cluster status info"))
532
# NOTE(jdg): The SF api provides an UNBELIEVABLE amount
533
# of stats data, this is just one of the calls
534
results = self._issue_api_request('GetClusterCapacity', params)
535
if 'result' not in results:
536
LOG.error(_('Failed to get updated stats'))
538
results = results['result']['clusterCapacity']
540
results['maxProvisionedSpace'] - results['usedSpace']
543
data["volume_backend_name"] = self.__class__.__name__
544
data["vendor_name"] = 'SolidFire Inc'
545
data["driver_version"] = '1.2'
546
data["storage_protocol"] = 'iSCSI'
548
data['total_capacity_gb'] = results['maxProvisionedSpace']
549
data['free_capacity_gb'] = free_capacity
550
data['reserved_percentage'] = FLAGS.reserved_percentage
551
data['QoS_support'] = True
552
data['compression_percent'] =\
553
results['compressionPercent']
554
data['deduplicaton_percent'] =\
555
results['deDuplicationPercent']
556
data['thin_provision_percent'] =\
557
results['thinProvisioningPercent']
558
self.cluster_stats = data