~ubuntu-cloud-archive/ubuntu/precise/cinder/trunk

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2012-11-23 08:39:28 UTC
  • mfrom: (1.1.9)
  • Revision ID: package-import@ubuntu.com-20121123083928-xvzet603cjfj9p1t
Tags: 2013.1~g1-0ubuntu1
* New upstream release.
* debian/patches/avoid_setuptools_git_dependency.patch:
  Avoid git installation. (LP: #1075948) 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright 2011 Justin Santa Barbara
 
4
# All Rights Reserved.
 
5
#
 
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
 
9
#
 
10
#         http://www.apache.org/licenses/LICENSE-2.0
 
11
#
 
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
 
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
 
 
24
import base64
 
25
import httplib
 
26
import json
 
27
import random
 
28
import socket
 
29
import string
 
30
import uuid
 
31
 
 
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
 
37
 
 
38
 
 
39
LOG = logging.getLogger(__name__)
 
40
 
 
41
sf_opts = [
 
42
    cfg.BoolOpt('sf_emulate_512',
 
43
                default=True,
 
44
                help='Set 512 byte emulation on volume creation; '),
 
45
 
 
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
    cfg.BoolOpt('sf_allow_tenant_qos',
 
59
               default=True,
 
60
               help='Allow tenants to specify QOS on create'), ]
 
61
 
 
62
FLAGS = flags.FLAGS
 
63
FLAGS.register_opts(sf_opts)
 
64
 
 
65
 
 
66
class SolidFire(SanISCSIDriver):
 
67
 
 
68
    sf_qos_dict = {'slow': {'minIOPS': 100,
 
69
                            'maxIOPS': 200,
 
70
                            'burstIOPS': 200},
 
71
                   'medium': {'minIOPS': 200,
 
72
                              'maxIOPS': 400,
 
73
                              'burstIOPS': 400},
 
74
                   'fast': {'minIOPS': 500,
 
75
                            'maxIOPS': 1000,
 
76
                            'burstIOPS': 1000},
 
77
                   'performant': {'minIOPS': 2000,
 
78
                                  'maxIOPS': 4000,
 
79
                                  'burstIOPS': 4000},
 
80
                   'off': None}
 
81
 
 
82
    def __init__(self, *args, **kwargs):
 
83
            super(SolidFire, self).__init__(*args, **kwargs)
 
84
 
 
85
    def _issue_api_request(self, method_name, params):
 
86
        """All API requests to SolidFire device go through this method
 
87
 
 
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.
 
91
        """
 
92
 
 
93
        host = FLAGS.san_ip
 
94
        # For now 443 is the only port our server accepts requests on
 
95
        port = 443
 
96
 
 
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
        cluster_admin = FLAGS.san_login
 
103
        cluster_password = FLAGS.san_password
 
104
 
 
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)
 
146
        return data
 
147
 
 
148
    def _get_volumes_by_sfaccount(self, account_id):
 
149
        params = {'accountID': account_id}
 
150
        data = self._issue_api_request('ListVolumesForAccount', params)
 
151
        if 'result' in data:
 
152
            return data['result']['volumes']
 
153
 
 
154
    def _get_sfaccount_by_name(self, sf_account_name):
 
155
        sfaccount = None
 
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']
 
161
        return sfaccount
 
162
 
 
163
    def _create_sfaccount(self, cinder_project_id):
 
164
        """Create account on SolidFire device if it doesn't already exist.
 
165
 
 
166
        We're first going to check if the account already exits, if it does
 
167
        just return it.  If not, then create it.
 
168
        """
 
169
 
 
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...'),
 
174
                      sf_account_name)
 
175
            chap_secret = self._generate_random_string(12)
 
176
            params = {'username': sf_account_name,
 
177
                      'initiatorSecret': chap_secret,
 
178
                      'targetSecret': chap_secret,
 
179
                      'attributes': {}}
 
180
            data = self._issue_api_request('AddAccount', params)
 
181
            if 'result' in data:
 
182
                sfaccount = self._get_sfaccount_by_name(sf_account_name)
 
183
 
 
184
        return sfaccount
 
185
 
 
186
    def _get_cluster_info(self):
 
187
        params = {}
 
188
        data = self._issue_api_request('GetClusterInfo', params)
 
189
        if 'result' not in data:
 
190
            raise exception.SolidFireAPIDataException(data=data)
 
191
 
 
192
        return data['result']
 
193
 
 
194
    def _do_export(self, volume):
 
195
        """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)
 
199
 
 
200
        model_update = {}
 
201
        model_update['provider_auth'] = ('CHAP %s %s'
 
202
                                         % (sfaccount['username'],
 
203
                                            sfaccount['targetSecret']))
 
204
 
 
205
        return model_update
 
206
 
 
207
    def _generate_random_string(self, length):
 
208
        """Generates random_string to use for CHAP password."""
 
209
 
 
210
        char_set = string.ascii_uppercase + string.digits
 
211
        return ''.join(random.sample(char_set, length))
 
212
 
 
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']
 
218
 
 
219
        params['accountID'] = sfaccount['accountID']
 
220
        data = self._issue_api_request('CreateVolume', params)
 
221
 
 
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
 
244
 
 
245
    def create_volume(self, volume):
 
246
        """Create volume on SolidFire device.
 
247
 
 
248
        The account is where CHAP settings are derived from, volume is
 
249
        created and exported.  Note that the new volume is immediately ready
 
250
        for use.
 
251
 
 
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.
 
257
 
 
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
        """
 
263
        GB = 1048576 * 1024
 
264
        slice_count = 1
 
265
        attributes = {}
 
266
        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'],
 
288
                  'accountID': None,
 
289
                  'sliceCount': slice_count,
 
290
                  'totalSize': volume['size'] * GB,
 
291
                  'enable512e': FLAGS.sf_emulate_512,
 
292
                  'attributes': attributes,
 
293
                  'qos': qos}
 
294
 
 
295
        return self._do_volume_create(volume['project_id'], params)
 
296
 
 
297
    def delete_volume(self, volume, is_snapshot=False):
 
298
        """Delete SolidFire Volume from device.
 
299
 
 
300
        SolidFire allows multipe volumes with same name,
 
301
        volumeID is what's guaranteed unique.
 
302
 
 
303
        """
 
304
 
 
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)
 
310
 
 
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)
 
315
 
 
316
        if is_snapshot:
 
317
            seek = 'OS-SNAPID-%s' % (volume['id'])
 
318
        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)
 
340
 
 
341
        LOG.debug(_("Leaving SolidFire delete_volume"))
 
342
 
 
343
    def ensure_export(self, context, volume):
 
344
        LOG.debug(_("Executing SolidFire ensure_export..."))
 
345
        return self._do_export(volume)
 
346
 
 
347
    def create_export(self, context, volume):
 
348
        LOG.debug(_("Executing SolidFire create_export..."))
 
349
        return self._do_export(volume)
 
350
 
 
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
    def delete_snapshot(self, snapshot):
 
388
        self.delete_volume(snapshot, True)
 
389
 
 
390
    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)
 
394
 
 
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']
 
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