~ubuntu-branches/ubuntu/trusty/cinder/trusty

« back to all changes in this revision

Viewing changes to cinder/volume/drivers/solidfire.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short, Yolanda Robla Mota, James Page, Chuck Short
  • Date: 2013-02-22 10:45:17 UTC
  • mfrom: (1.1.11)
  • Revision ID: package-import@ubuntu.com-20130222104517-ng3r6ace9vi4m869
Tags: 2013.1.g3-0ubuntu1
[ Yolanda Robla Mota ]
* d/control: Add BD on python-hp3parclient.
* d/patches: Drop patches related to disabling hp3parclient.

[ James Page ]
* d/control: Add Suggests: python-hp3parclient for python-cinder.
* d/control: Add BD on python-oslo-config.
* d/*: Wrapped and sorted.

[ Chuck Short ]
* New upstream release.
* debian/rules, debian/cinder-volumes.install: 
  - Fail if binaries are missing and install missing binaries.
* debian/patches/fix-ubuntu-tests.patch: Fix failing tests.
* debian/control: Add python-rtslib and python-mock.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
15
#    License for the specific language governing permissions and limitations
16
16
#    under the License.
17
 
"""
18
 
Drivers for san-stored volumes.
19
 
 
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.
22
 
"""
23
17
 
24
18
import base64
25
19
import httplib
26
20
import json
 
21
import math
27
22
import random
28
23
import socket
29
24
import string
 
25
import time
30
26
import uuid
31
27
 
 
28
from oslo.config import cfg
 
29
 
 
30
from cinder import context
32
31
from cinder import exception
33
32
from cinder import flags
34
 
from cinder.openstack.common import cfg
35
33
from cinder.openstack.common import log as logging
36
34
from cinder.volume.drivers.san.san import SanISCSIDriver
37
 
 
38
 
 
 
35
from cinder.volume import volume_types
 
36
 
 
37
VERSION = 1.2
39
38
LOG = logging.getLogger(__name__)
40
39
 
41
40
sf_opts = [
43
42
                default=True,
44
43
                help='Set 512 byte emulation on volume creation; '),
45
44
 
46
 
    cfg.StrOpt('sf_mvip',
47
 
               default='',
48
 
               help='IP address of SolidFire MVIP'),
49
 
 
50
 
    cfg.StrOpt('sf_login',
51
 
               default='admin',
52
 
               help='Username for SF Cluster Admin'),
53
 
 
54
 
    cfg.StrOpt('sf_password',
55
 
               default='',
56
 
               help='Password for SF Cluster Admin'),
57
 
 
58
45
    cfg.BoolOpt('sf_allow_tenant_qos',
59
 
                default=True,
 
46
                default=False,
60
47
                help='Allow tenants to specify QOS on create'), ]
61
48
 
62
49
FLAGS = flags.FLAGS
64
51
 
65
52
 
66
53
class SolidFire(SanISCSIDriver):
 
54
    """OpenStack driver to enable SolidFire cluster.
 
55
 
 
56
    Version history:
 
57
        1.0 - Initial driver
 
58
        1.1 - Refactor, clone support, qos by type and minor bug fixes
 
59
 
 
60
    """
67
61
 
68
62
    sf_qos_dict = {'slow': {'minIOPS': 100,
69
63
                            'maxIOPS': 200,
79
73
                                  'burstIOPS': 4000},
80
74
                   'off': None}
81
75
 
 
76
    sf_qos_keys = ['minIOPS', 'maxIOPS', 'burstIOPS']
 
77
    cluster_stats = {}
 
78
 
 
79
    GB = math.pow(10, 9)
 
80
 
82
81
    def __init__(self, *args, **kwargs):
83
82
            super(SolidFire, self).__init__(*args, **kwargs)
 
83
            self._update_cluster_status()
84
84
 
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.
87
87
 
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.
 
91
 
91
92
        """
92
 
 
 
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
95
99
        port = 443
96
100
 
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
101
 
 
102
101
        cluster_admin = FLAGS.san_login
103
102
        cluster_password = FLAGS.san_password
104
103
 
105
 
        command = {'method': method_name,
106
 
                   'id': request_id}
107
 
 
108
 
        if params is not None:
109
 
            command['params'] = params
110
 
 
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'}
115
 
 
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
122
 
 
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()
127
 
        data = {}
128
 
 
129
 
        if response.status != 200:
130
 
            connection.close()
131
 
            raise exception.SolidFireAPIException(status=response.status)
132
 
 
133
 
        else:
134
 
            data = response.read()
135
 
            try:
136
 
                data = json.loads(data)
137
 
 
138
 
            except (TypeError, ValueError), exc:
139
 
                connection.close()
140
 
                msg = _("Call to json.loads() raised an exception: %s") % exc
141
 
                raise exception.SfJsonEncodeFailure(msg)
142
 
 
143
 
            connection.close()
144
 
 
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
 
107
        # can't be re-used
 
108
        retry_count = 5
 
109
        while retry_count > 0:
 
110
            request_id = int(uuid.uuid4())  # just generate a random number
 
111
            command = {'method': method_name,
 
112
                       'id': request_id}
 
113
 
 
114
            if params is not None:
 
115
                command['params'] = params
 
116
 
 
117
            payload = json.dumps(command, ensure_ascii=False)
 
118
            payload.encode('utf-8')
 
119
            header = {'Content-Type': 'application/json-rpc; charset=utf-8'}
 
120
 
 
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
 
127
 
 
128
            LOG.debug(_("Payload for SolidFire API call: %s"), payload)
 
129
 
 
130
            connection = httplib.HTTPSConnection(host, port)
 
131
            connection.request('POST', '/json-rpc/1.0', payload, header)
 
132
            response = connection.getresponse()
 
133
 
 
134
            data = {}
 
135
            if response.status != 200:
 
136
                connection.close()
 
137
                raise exception.SolidFireAPIException(status=response.status)
 
138
 
 
139
            else:
 
140
                data = response.read()
 
141
                try:
 
142
                    data = json.loads(data)
 
143
                except (TypeError, ValueError), exc:
 
144
                    connection.close()
 
145
                    msg = _("Call to json.loads() raised "
 
146
                            "an exception: %s") % exc
 
147
                    raise exception.SfJsonEncodeFailure(msg)
 
148
 
 
149
                connection.close()
 
150
 
 
151
            LOG.debug(_("Results of SolidFire API call: %s"), data)
 
152
 
 
153
            if 'error' in data:
 
154
                if data['error']['name'] in max_simultaneous_clones:
 
155
                    LOG.warning(_('Clone operation '
 
156
                                  'encountered: %s') % data['error']['name'])
 
157
                    LOG.warning(_(
 
158
                        'Waiting for outstanding operation '
 
159
                        'before retrying snapshot: %s') % params['name'])
 
160
                    time.sleep(5)
 
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))
 
165
                    time.sleep(1)
 
166
                    retry_count -= 1
 
167
                else:
 
168
                    msg = _("API response: %s") % data
 
169
                    raise exception.SolidFireAPIException(msg)
 
170
            else:
 
171
                retry_count = 0
 
172
 
146
173
        return data
147
174
 
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']
153
181
 
154
182
    def _get_sfaccount_by_name(self, sf_account_name):
 
183
        """Get SolidFire account object by name."""
155
184
        sfaccount = None
156
185
        params = {'username': sf_account_name}
157
186
        data = self._issue_api_request('GetAccountByName', params)
160
189
            sfaccount = data['result']['account']
161
190
        return sfaccount
162
191
 
163
 
    def _create_sfaccount(self, cinder_project_id):
 
192
    def _get_sf_account_name(self, project_id):
 
193
        """Build the SolidFire account name to use."""
 
194
        return ('%s-%s' % (socket.gethostname(), project_id))
 
195
 
 
196
    def _get_sfaccount(self, project_id):
 
197
        sf_account_name = self._get_sf_account_name(project_id)
 
198
        sfaccount = self._get_sfaccount_by_name(sf_account_name)
 
199
        if sfaccount is None:
 
200
            raise exception.SfAccountNotFound(account_name=sf_account_name)
 
201
 
 
202
        return sfaccount
 
203
 
 
204
    def _create_sfaccount(self, project_id):
164
205
        """Create account on SolidFire device if it doesn't already exist.
165
206
 
166
207
        We're first going to check if the account already exits, if it does
167
208
        just return it.  If not, then create it.
 
209
 
168
210
        """
169
211
 
170
 
        sf_account_name = socket.gethostname() + '-' + cinder_project_id
 
212
        sf_account_name = self._get_sf_account_name(project_id)
171
213
        sfaccount = self._get_sfaccount_by_name(sf_account_name)
172
214
        if sfaccount is None:
173
215
            LOG.debug(_('solidfire account: %s does not exist, create it...'),
184
226
        return sfaccount
185
227
 
186
228
    def _get_cluster_info(self):
 
229
        """Query the SolidFire cluster for some property info."""
187
230
        params = {}
188
231
        data = self._issue_api_request('GetClusterInfo', params)
189
232
        if 'result' not in data:
193
236
 
194
237
    def _do_export(self, volume):
195
238
        """Gets the associated account, retrieves CHAP info and updates."""
196
 
 
197
 
        sfaccount_name = '%s-%s' % (socket.gethostname(), volume['project_id'])
198
 
        sfaccount = self._get_sfaccount_by_name(sfaccount_name)
 
239
        sfaccount = self._get_sfaccount(volume['project_id'])
199
240
 
200
241
        model_update = {}
201
242
        model_update['provider_auth'] = ('CHAP %s %s'
210
251
        char_set = string.ascii_uppercase + string.digits
211
252
        return ''.join(random.sample(char_set, length))
212
253
 
 
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']
 
259
 
 
260
        volume_list = self._get_volumes_by_sfaccount(sfaccount['accountID'])
 
261
        iqn = None
 
262
        for v in volume_list:
 
263
            if v['volumeID'] == sf_volume_id:
 
264
                iqn = v['iqn']
 
265
                break
 
266
 
 
267
        model_update = {}
 
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'],
 
273
                                         chap_secret))
 
274
        return model_update
 
275
 
 
276
    def _do_clone_volume(self, src_uuid, src_project_id, v_ref):
 
277
        """Create a clone of an existing volume.
 
278
 
 
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.
 
284
 
 
285
        """
 
286
        attributes = {}
 
287
        qos = {}
 
288
 
 
289
        sfaccount = self._get_sfaccount(src_project_id)
 
290
        params = {'accountID': sfaccount['accountID']}
 
291
 
 
292
        sf_vol = self._get_sf_volume(src_uuid, params)
 
293
        if sf_vol is None:
 
294
            raise exception.VolumeNotFound(volume_id=uuid)
 
295
 
 
296
        if 'qos' in sf_vol:
 
297
            qos = sf_vol['qos']
 
298
 
 
299
        attributes = {'uuid': v_ref['id'],
 
300
                      'is_clone': 'True',
 
301
                      'src_uuid': 'src_uuid'}
 
302
 
 
303
        if qos:
 
304
            attributes['qos'] = qos
 
305
 
 
306
        params = {'volumeID': int(sf_vol['volumeID']),
 
307
                  'name': 'UUID-%s' % v_ref['id'],
 
308
                  'attributes': attributes,
 
309
                  'qos': qos}
 
310
 
 
311
        data = self._issue_api_request('CloneVolume', params)
 
312
 
 
313
        if (('result' not in data) or ('volumeID' not in data['result'])):
 
314
            raise exception.SolidFireAPIDataException(data=data)
 
315
 
 
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)
 
321
 
 
322
        return (data, sfaccount, model_update)
 
323
 
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']
218
326
 
219
327
        params['accountID'] = sfaccount['accountID']
220
328
        data = self._issue_api_request('CreateVolume', params)
221
329
 
222
 
        if 'result' not in data or 'volumeID' not in data['result']:
223
 
            raise exception.SolidFireAPIDataException(data=data)
224
 
 
225
 
        volume_id = data['result']['volumeID']
226
 
 
227
 
        volume_list = self._get_volumes_by_sfaccount(sfaccount['accountID'])
228
 
        iqn = None
229
 
        for v in volume_list:
230
 
            if v['volumeID'] == volume_id:
231
 
                iqn = v['iqn']
232
 
                break
233
 
 
234
 
        model_update = {}
235
 
 
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'],
241
 
                                         chap_secret))
242
 
 
243
 
        return model_update
 
330
        if (('result' not in data) or ('volumeID' not in data['result'])):
 
331
            raise exception.SolidFireAPIDataException(data=data)
 
332
 
 
333
        sf_volume_id = data['result']['volumeID']
 
334
        return self._get_model_info(sfaccount, sf_volume_id)
 
335
 
 
336
    def _set_qos_presets(self, volume):
 
337
        qos = {}
 
338
        valid_presets = self.sf_qos_dict.keys()
 
339
 
 
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]
 
343
        if len(presets) > 0:
 
344
            if len(presets) > 1:
 
345
                LOG.warning(_('More than one valid preset was '
 
346
                              'detected, using %s') % presets[0])
 
347
            qos = self.sf_qos_dict[presets[0]]
 
348
        else:
 
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)
 
353
        return qos
 
354
 
 
355
    def _set_qos_by_volume_type(self, type_id, ctxt):
 
356
        qos = {}
 
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)
 
362
        return qos
 
363
 
 
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)
 
368
 
 
369
        found_count = 0
 
370
        sf_volref = None
 
371
        for v in data['result']['volumes']:
 
372
            if uuid in v['name']:
 
373
                found_count += 1
 
374
                sf_volref = v
 
375
                LOG.debug(_("Mapped SolidFire volumeID %(sfid)s "
 
376
                            "to cinder ID %(uuid)s.") %
 
377
                          {'sfid': v['volumeID'],
 
378
                           'uuid': uuid})
 
379
 
 
380
        if found_count == 0:
 
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)
 
385
 
 
386
        if found_count > 1:
 
387
            LOG.error(_("Found %(count)s volumes mapped to id: %(uuid)s.") %
 
388
                      {'count': found_count,
 
389
                       'uuid': uuid})
 
390
            raise exception.DuplicateSfVolumeNames(vol_name=uuid)
 
391
 
 
392
        return sf_volref
244
393
 
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.
257
406
 
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.
262
407
        """
263
 
        GB = 1048576 * 1024
264
408
        slice_count = 1
265
409
        attributes = {}
266
410
        qos = {}
267
 
        qos_keys = ['minIOPS', 'maxIOPS', 'burstIOPS']
268
 
        valid_presets = self.sf_qos_dict.keys()
269
 
 
270
 
        if FLAGS.sf_allow_tenant_qos and \
271
 
                volume.get('volume_metadata')is not None:
272
 
 
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]
276
 
            if len(presets) > 0:
277
 
                if len(presets) > 1:
278
 
                    LOG.warning(_('More than one valid preset was '
279
 
                                  'detected, using %s') % presets[0])
280
 
                qos = self.sf_qos_dict[presets[0]]
281
 
            else:
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)
286
 
 
287
 
        params = {'name': 'OS-VOLID-%s' % volume['id'],
 
411
 
 
412
        if (FLAGS.sf_allow_tenant_qos and
 
413
                volume.get('volume_metadata')is not None):
 
414
            qos = self._set_qos_presets(volume)
 
415
 
 
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)
 
420
 
 
421
        attributes = {'uuid': volume['id'],
 
422
                      'is_clone': 'False'}
 
423
        if qos:
 
424
            attributes['qos'] = qos
 
425
 
 
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,
293
432
                  'qos': qos}
294
433
 
295
434
        return self._do_volume_create(volume['project_id'], params)
296
435
 
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(
 
439
            src_vref['id'],
 
440
            src_vref['project_id'],
 
441
            volume)
 
442
 
 
443
        return model
 
444
 
 
445
    def delete_volume(self, volume):
298
446
        """Delete SolidFire Volume from device.
299
447
 
300
448
        SolidFire allows multipe volumes with same name,
303
451
        """
304
452
 
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)
310
454
 
 
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)
315
 
 
316
 
        if is_snapshot:
317
 
            seek = 'OS-SNAPID-%s' % (volume['id'])
 
457
 
 
458
        sf_vol = self._get_sf_volume(volume['id'], params)
 
459
 
 
460
        if sf_vol is not None:
 
461
            params = {'volumeID': sf_vol['volumeID']}
 
462
            data = self._issue_api_request('DeleteVolume', params)
 
463
 
 
464
            if 'result' not in data:
 
465
                raise exception.SolidFireAPIDataException(data=data)
318
466
        else:
319
 
            seek = 'OS-VOLID-%s' % volume['id']
320
 
            #params = {'name': 'OS-VOLID-:%s' % volume['id'],
321
 
 
322
 
        found_count = 0
323
 
        volid = -1
324
 
        for v in data['result']['volumes']:
325
 
            if v['name'] == seek:
326
 
                found_count += 1
327
 
                volid = v['volumeID']
328
 
 
329
 
        if found_count == 0:
330
 
            raise exception.VolumeNotFound(volume_id=volume['id'])
331
 
 
332
 
        if found_count > 1:
333
 
            LOG.debug(_("Deleting volumeID: %s"), volid)
334
 
            raise exception.DuplicateSfVolumeNames(vol_name=volume['id'])
335
 
 
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'])
340
469
 
341
470
        LOG.debug(_("Leaving SolidFire delete_volume"))
342
471
 
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)
346
476
 
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)
350
481
 
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)
358
 
 
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)
363
 
 
364
 
        found_count = 0
365
 
        volid = -1
366
 
        for v in data['result']['volumes']:
367
 
            if v['name'] == 'OS-VOLID-%s' % snapshot['volume_id']:
368
 
                found_count += 1
369
 
                volid = v['volumeID']
370
 
 
371
 
        if found_count == 0:
372
 
            raise exception.VolumeNotFound(volume_id=snapshot['volume_id'])
373
 
        if found_count != 1:
374
 
            raise exception.DuplicateSfVolumeNames(
375
 
                vol_name='OS-VOLID-%s' % snapshot['volume_id'])
376
 
 
377
 
        params = {'volumeID': int(volid),
378
 
                  'name': snapshot_name,
379
 
                  'attributes': {'OriginatingVolume': volid}}
380
 
 
381
 
        data = self._issue_api_request('CloneVolume', params)
382
 
        if 'result' not in data:
383
 
            raise exception.SolidFireAPIDataException(data=data)
384
 
 
385
 
        return (data, sfaccount)
386
 
 
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)
389
485
 
390
486
    def create_snapshot(self, snapshot):
391
 
        snapshot_name = 'OS-SNAPID-%s' % (
392
 
                        snapshot['id'])
393
 
        (data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name)
 
487
        """Create a snapshot of a volume on the SolidFire cluster.
 
488
 
 
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.
 
493
 
 
494
        After GA a true snapshot implementation will be available with
 
495
        restore at which time we'll rework this appropriately.
 
496
 
 
497
        """
 
498
        (data, sfaccount, model) = self._do_clone_volume(
 
499
            snapshot['volume_id'],
 
500
            snapshot['project_id'],
 
501
            snapshot)
394
502
 
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']
401
 
 
402
 
        (data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name)
403
 
 
404
 
        if 'result' not in data or 'volumeID' not in data['result']:
405
 
            raise exception.SolidFireAPIDataException(data=data)
406
 
 
407
 
        volume_id = data['result']['volumeID']
408
 
        volume_list = self._get_volumes_by_sfaccount(sf_account['accountID'])
409
 
        iqn = None
410
 
        for v in volume_list:
411
 
            if v['volumeID'] == volume_id:
412
 
                iqn = v['iqn']
413
 
                break
414
 
 
415
 
        model_update = {}
416
 
 
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'],
422
 
                                            chap_secret))
423
 
        return model_update
 
504
        """Create a volume from the specified snapshot."""
 
505
        (data, sfaccount, model) = self._do_clone_volume(
 
506
            snapshot['id'],
 
507
            snapshot['project_id'],
 
508
            volume)
 
509
 
 
510
        return model
 
511
 
 
512
    def get_volume_stats(self, refresh=False):
 
513
        """Get volume status.
 
514
 
 
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
 
518
        data
 
519
        """
 
520
        if refresh:
 
521
            self._update_cluster_status()
 
522
 
 
523
        return self.cluster_stats
 
524
 
 
525
    def _update_cluster_status(self):
 
526
        """Retrieve status info for the Cluster."""
 
527
 
 
528
        LOG.debug(_("Updating cluster status info"))
 
529
 
 
530
        params = {}
 
531
 
 
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'))
 
537
 
 
538
        results = results['result']['clusterCapacity']
 
539
        free_capacity =\
 
540
            results['maxProvisionedSpace'] - results['usedSpace']
 
541
 
 
542
        data = {}
 
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'
 
547
 
 
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