~ubuntu-branches/ubuntu/utopic/cinder/utopic

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Chuck Short, James Page, Adam Gandelman, Chuck Short
  • Date: 2013-09-08 21:09:46 UTC
  • mfrom: (1.1.18)
  • Revision ID: package-import@ubuntu.com-20130908210946-3dbzq1jy5uji4wad
Tags: 1:2013.2~b3-0ubuntu1
[ James Page ]
* d/control: Switch ceph-common -> python-ceph inline with upstream
  refactoring of Ceph RBD driver, move to Suggests of python-cinder.
  (LP: #1190791). 

[ Adam Gandelman ]
* debian/patches/avoid_paramiko_vers_depends.patch: Dropped, no longer
  required.
* Add minimum requirement python-greenlet (>= 0.3.2).
* Add minimum requirement python-eventlet (>= 0.12.0).
* Add minimum requirement python-paramiko (>= 1.8).

[ Chuck Short ]
* New upstream release.
* debian/patches/skip-sqlachemy-failures.patch: Skip testfailures
  with sqlalchemy 0.8 until they are fixed upstream.
* debian/control: Add python-babel to build-depends.
* debian/control: Add python-novaclient to build-depends.

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
Desc    : Driver to store volumes on Coraid Appliances.
19
19
Require : Coraid EtherCloud ESM, Coraid VSX and Coraid SRX.
20
20
Author  : Jean-Baptiste RANSY <openstack@alyseo.com>
 
21
Author  : Alex Zasimov <azasimov@mirantis.com>
 
22
Author  : Nikolay Sobolevsky <nsobolevsky@mirantis.com>
21
23
Contrib : Larry Matter <support@coraid.com>
22
24
"""
23
25
 
24
26
import cookielib
25
 
import time
 
27
import math
 
28
import urllib
26
29
import urllib2
 
30
import urlparse
27
31
 
28
32
from oslo.config import cfg
29
33
 
 
34
from cinder import exception
30
35
from cinder.openstack.common import jsonutils
 
36
from cinder.openstack.common import lockutils
31
37
from cinder.openstack.common import log as logging
 
38
from cinder import units
32
39
from cinder.volume import driver
33
40
from cinder.volume import volume_types
34
41
 
57
64
CONF.register_opts(coraid_opts)
58
65
 
59
66
 
60
 
class CoraidException(Exception):
61
 
    def __init__(self, message=None, error=None):
62
 
        super(CoraidException, self).__init__(message, error)
63
 
 
64
 
    def __str__(self):
65
 
        return '%s: %s' % self.args
66
 
 
67
 
 
68
 
class CoraidRESTException(CoraidException):
69
 
    pass
70
 
 
71
 
 
72
 
class CoraidESMException(CoraidException):
73
 
    pass
 
67
ESM_SESSION_EXPIRED_STATES = ['GeneralAdminFailure',
 
68
                              'passwordInactivityTimeout',
 
69
                              'passwordAbsoluteTimeout']
74
70
 
75
71
 
76
72
class CoraidRESTClient(object):
77
 
    """Executes volume driver commands on Coraid ESM EtherCloud Appliance."""
78
 
 
79
 
    def __init__(self, ipaddress, user, group, password):
80
 
        self.url = "https://%s:8443/" % ipaddress
81
 
        self.user = user
82
 
        self.group = group
83
 
        self.password = password
84
 
        self.session = False
85
 
        self.cookiejar = cookielib.CookieJar()
86
 
        self.urlOpener = urllib2.build_opener(
87
 
            urllib2.HTTPCookieProcessor(self.cookiejar))
88
 
        LOG.debug(_('Running with CoraidDriver for ESM EtherCLoud'))
 
73
    """Executes REST RPC requests on Coraid ESM EtherCloud Appliance."""
 
74
 
 
75
    def __init__(self, esm_url):
 
76
        self._check_esm_url(esm_url)
 
77
        self._esm_url = esm_url
 
78
        self._cookie_jar = cookielib.CookieJar()
 
79
        self._url_opener = urllib2.build_opener(
 
80
            urllib2.HTTPCookieProcessor(self._cookie_jar))
 
81
 
 
82
    def _check_esm_url(self, esm_url):
 
83
        splitted = urlparse.urlsplit(esm_url)
 
84
        if splitted.scheme != 'https':
 
85
            raise ValueError(
 
86
                _('Invalid ESM url scheme "%s". Supported https only.') %
 
87
                splitted.scheme)
 
88
 
 
89
    @lockutils.synchronized('coraid_rpc', 'cinder-', False)
 
90
    def rpc(self, handle, url_params, data, allow_empty_response=False):
 
91
        return self._rpc(handle, url_params, data, allow_empty_response)
 
92
 
 
93
    def _rpc(self, handle, url_params, data, allow_empty_response):
 
94
        """Execute REST RPC using url <esm_url>/handle?url_params.
 
95
 
 
96
        Send JSON encoded data in body of POST request.
 
97
 
 
98
        Exceptions:
 
99
            urllib2.URLError
 
100
              1. Name or service not found (e.reason is socket.gaierror)
 
101
              2. Socket blocking operation timeout (e.reason is
 
102
                 socket.timeout)
 
103
              3. Network IO error (e.reason is socket.error)
 
104
 
 
105
            urllib2.HTTPError
 
106
              1. HTTP 404, HTTP 500 etc.
 
107
 
 
108
            CoraidJsonEncodeFailure - bad REST response
 
109
        """
 
110
        # Handle must be simple path, for example:
 
111
        #    /configure
 
112
        if '?' in handle or '&' in handle:
 
113
            raise ValueError(_('Invalid REST handle name. Expected path.'))
 
114
 
 
115
        # Request url includes base ESM url, handle path and optional
 
116
        # URL params.
 
117
        rest_url = urlparse.urljoin(self._esm_url, handle)
 
118
        encoded_url_params = urllib.urlencode(url_params)
 
119
        if encoded_url_params:
 
120
            rest_url += '?' + encoded_url_params
 
121
 
 
122
        if data is None:
 
123
            json_request = None
 
124
        else:
 
125
            json_request = jsonutils.dumps(data)
 
126
 
 
127
        request = urllib2.Request(rest_url, json_request)
 
128
        response = self._url_opener.open(request).read()
 
129
 
 
130
        try:
 
131
            if not response and allow_empty_response:
 
132
                reply = {}
 
133
            else:
 
134
                reply = jsonutils.loads(response)
 
135
        except (TypeError, ValueError) as exc:
 
136
            msg = (_('Call to json.loads() failed: %(ex)s.'
 
137
                     ' Response: %(resp)s') %
 
138
                   {'ex': exc, 'resp': response})
 
139
            raise exception.CoraidJsonEncodeFailure(msg)
 
140
 
 
141
        return reply
 
142
 
 
143
 
 
144
def to_coraid_kb(gb):
 
145
    return math.ceil(float(gb) * units.GiB / 1000)
 
146
 
 
147
 
 
148
def coraid_volume_size(gb):
 
149
    return '{0}K'.format(to_coraid_kb(gb))
 
150
 
 
151
 
 
152
class CoraidAppliance(object):
 
153
    def __init__(self, rest_client, username, password, group):
 
154
        self._rest_client = rest_client
 
155
        self._username = username
 
156
        self._password = password
 
157
        self._group = group
 
158
        self._logined = False
89
159
 
90
160
    def _login(self):
91
 
        """Login and Session Handler."""
92
 
        if not self.session or self.session < time.time():
93
 
            url = ('admin?op=login&username=%s&password=%s' %
94
 
                   (self.user, self.password))
95
 
            data = 'Login'
96
 
            reply = self._admin_esm_cmd(url, data)
97
 
            if reply.get('state') == 'adminSucceed':
98
 
                self.session = time.time() + 1100
99
 
                msg = _('Update session cookie %(session)s')
100
 
                LOG.debug(msg % dict(session=self.session))
101
 
                self._set_group(reply)
102
 
                return True
103
 
            else:
104
 
                errmsg = reply.get('message', '')
105
 
                msg = _('Message : %(message)s')
106
 
                raise CoraidESMException(msg % dict(message=errmsg))
107
 
        return True
108
 
 
109
 
    def _set_group(self, reply):
110
 
        """Set effective group."""
111
 
        if self.group:
112
 
            group = self.group
113
 
            groupId = self._get_group_id(group, reply)
114
 
            if groupId:
115
 
                url = ('admin?op=setRbacGroup&groupId=%s' % (groupId))
116
 
                data = 'Group'
117
 
                reply = self._admin_esm_cmd(url, data)
118
 
                if reply.get('state') == 'adminSucceed':
119
 
                    return True
120
 
                else:
121
 
                    errmsg = reply.get('message', '')
122
 
                    msg = _('Error while trying to set group: %(message)s')
123
 
                    raise CoraidRESTException(msg % dict(message=errmsg))
124
 
            else:
125
 
                msg = _('Unable to find group: %(group)s')
126
 
                raise CoraidESMException(msg % dict(group=group))
127
 
        return True
128
 
 
129
 
    def _get_group_id(self, groupName, loginResult):
130
 
        """Map group name to group ID."""
131
 
        # NOTE(lmatter): All other groups are under the admin group
132
 
        fullName = "admin group:%s" % groupName
133
 
        groupId = False
134
 
        for kid in loginResult['values']:
135
 
            fullPath = kid['fullPath']
136
 
            if fullPath == fullName:
137
 
                return kid['groupId']
138
 
        return False
139
 
 
140
 
    def _esm_cmd(self, url=False, data=None):
141
 
        self._login()
142
 
        return self._admin_esm_cmd(url, data)
143
 
 
144
 
    def _admin_esm_cmd(self, url=False, data=None):
145
 
        """
146
 
        _admin_esm_cmd represent the entry point to send requests to ESM
147
 
        Appliance.  Send the HTTPS call, get response in JSON
148
 
        convert response into Python Object and return it.
149
 
        """
150
 
        if url:
151
 
            url = self.url + url
152
 
 
153
 
            req = urllib2.Request(url, data)
154
 
 
155
 
            try:
156
 
                res = self.urlOpener.open(req).read()
157
 
            except Exception:
158
 
                raise CoraidRESTException(_('ESM urlOpen error'))
159
 
 
160
 
            try:
161
 
                res_json = jsonutils.loads(res)
162
 
            except Exception:
163
 
                raise CoraidRESTException(_('JSON Error'))
164
 
 
165
 
            return res_json
166
 
        else:
167
 
            raise CoraidRESTException(_('Request without URL'))
168
 
 
169
 
    def _check_esm_alive(self):
170
 
        try:
171
 
            url = self.url + 'fetch'
172
 
            req = urllib2.Request(url)
173
 
            code = self.urlOpener.open(req).getcode()
174
 
            if code == '200':
175
 
                return True
176
 
            return False
177
 
        except Exception:
178
 
            return False
179
 
 
180
 
    def _configure(self, data):
181
 
        """In charge of all commands into 'configure'."""
182
 
        url = 'configure'
183
 
        LOG.debug(_('Configure data : %s'), data)
184
 
        response = self._esm_cmd(url, data)
185
 
        LOG.debug(_("Configure response : %s"), response)
186
 
        if response:
187
 
            if response.get('configState') == 'completedSuccessfully':
188
 
                return True
189
 
            else:
190
 
                errmsg = response.get('message', '')
191
 
                msg = _('Message : %(message)s')
192
 
                raise CoraidESMException(msg % dict(message=errmsg))
193
 
        return False
194
 
 
195
 
    def _get_volume_info(self, volume_name):
196
 
        """Retrive volume informations for a given volume name."""
197
 
        url = 'fetch?shelf=cms&orchStrRepo&lv=%s' % (volume_name)
198
 
        try:
199
 
            response = self._esm_cmd(url)
200
 
            info = response[0][1]['reply'][0]
201
 
            return {"pool": info['lv']['containingPool'],
202
 
                    "repo": info['repoName'],
203
 
                    "vsxidx": info['lv']['lunIndex'],
204
 
                    "index": info['lv']['lvStatus']['exportedLun']['lun'],
205
 
                    "shelf": info['lv']['lvStatus']['exportedLun']['shelf']}
206
 
        except Exception:
207
 
            msg = _('Unable to retrive volume infos for volume %(volname)s')
208
 
            raise CoraidESMException(msg % dict(volname=volume_name))
209
 
 
210
 
    def _get_lun_address(self, volume_name):
211
 
        """Return AoE Address for a given Volume."""
212
 
        volume_info = self._get_volume_info(volume_name)
213
 
        shelf = volume_info['shelf']
214
 
        lun = volume_info['index']
215
 
        return {'shelf': shelf, 'lun': lun}
216
 
 
217
 
    def create_lun(self, volume_name, volume_size, repository):
218
 
        """Create LUN on Coraid Backend Storage."""
219
 
        data = '[{"addr":"cms","data":"{' \
220
 
               '\\"servers\\":[\\"\\"],' \
221
 
               '\\"repoName\\":\\"%s\\",' \
222
 
               '\\"size\\":\\"%sG\\",' \
223
 
               '\\"lvName\\":\\"%s\\"}",' \
224
 
               '"op":"orchStrLun",' \
225
 
               '"args":"add"}]' % (repository, volume_size,
226
 
                                   volume_name)
227
 
        return self._configure(data)
 
161
        """Login into ESM.
 
162
 
 
163
        Perform login request and return available groups.
 
164
 
 
165
        :returns: dict -- map with group_name to group_id
 
166
        """
 
167
        ADMIN_GROUP_PREFIX = 'admin group:'
 
168
 
 
169
        url_params = {'op': 'login',
 
170
                      'username': self._username,
 
171
                      'password': self._password}
 
172
        reply = self._rest_client.rpc('admin', url_params, 'Login')
 
173
        if reply['state'] != 'adminSucceed':
 
174
            raise exception.CoraidESMBadCredentials()
 
175
 
 
176
        # Read groups map from login reply.
 
177
        groups_map = {}
 
178
        for group_info in reply.get('values', []):
 
179
            full_group_name = group_info['fullPath']
 
180
            if full_group_name.startswith(ADMIN_GROUP_PREFIX):
 
181
                group_name = full_group_name[len(ADMIN_GROUP_PREFIX):]
 
182
                groups_map[group_name] = group_info['groupId']
 
183
 
 
184
        return groups_map
 
185
 
 
186
    def _set_effective_group(self, groups_map, group):
 
187
        """Set effective group.
 
188
 
 
189
        Use groups_map returned from _login method.
 
190
        """
 
191
        try:
 
192
            group_id = groups_map[group]
 
193
        except KeyError:
 
194
            raise exception.CoraidESMBadGroup(group_name=group)
 
195
 
 
196
        url_params = {'op': 'setRbacGroup',
 
197
                      'groupId': group_id}
 
198
        reply = self._rest_client.rpc('admin', url_params, 'Group')
 
199
        if reply['state'] != 'adminSucceed':
 
200
            raise exception.CoraidESMBadCredentials()
 
201
 
 
202
        self._logined = True
 
203
 
 
204
    def _ensure_session(self):
 
205
        if not self._logined:
 
206
            groups_map = self._login()
 
207
            self._set_effective_group(groups_map, self._group)
 
208
 
 
209
    def _relogin(self):
 
210
        self._logined = False
 
211
        self._ensure_session()
 
212
 
 
213
    def rpc(self, handle, url_params, data, allow_empty_response=False):
 
214
        self._ensure_session()
 
215
 
 
216
        relogin_attempts = 3
 
217
        # Do action, relogin if needed and repeat action.
 
218
        while True:
 
219
            reply = self._rest_client.rpc(handle, url_params, data,
 
220
                                          allow_empty_response)
 
221
 
 
222
            if self._is_session_expired(reply):
 
223
                relogin_attempts -= 1
 
224
                if relogin_attempts <= 0:
 
225
                    raise exception.CoraidESMReloginFailed()
 
226
                LOG.debug(_('Session is expired. Relogin on ESM.'))
 
227
                self._relogin()
 
228
            else:
 
229
                return reply
 
230
 
 
231
    def _is_session_expired(self, reply):
 
232
        return ('state' in reply and
 
233
                reply['state'] in ESM_SESSION_EXPIRED_STATES and
 
234
                reply['metaCROp'] == 'reboot')
 
235
 
 
236
    def _is_bad_config_state(self, reply):
 
237
        return (not reply or
 
238
                'configState' not in reply or
 
239
                reply['configState'] != 'completedSuccessfully')
 
240
 
 
241
    def configure(self, json_request):
 
242
        reply = self.rpc('configure', {}, json_request)
 
243
        if self._is_bad_config_state(reply):
 
244
            # Calculate error message
 
245
            if not reply:
 
246
                message = _('Reply is empty.')
 
247
            else:
 
248
                message = reply.get('message', _('Error message is empty.'))
 
249
            raise exception.CoraidESMConfigureError(message=message)
 
250
        return reply
 
251
 
 
252
    def esm_command(self, request):
 
253
        request['data'] = jsonutils.dumps(request['data'])
 
254
        return self.configure([request])
 
255
 
 
256
    def get_volume_info(self, volume_name):
 
257
        """Retrieve volume information for a given volume name."""
 
258
        url_params = {'shelf': 'cms',
 
259
                      'orchStrRepo': '',
 
260
                      'lv': volume_name}
 
261
        reply = self.rpc('fetch', url_params, None)
 
262
        try:
 
263
            volume_info = reply[0][1]['reply'][0]
 
264
        except (IndexError, KeyError):
 
265
            raise exception.VolumeNotFound(volume_id=volume_name)
 
266
        return {'pool': volume_info['lv']['containingPool'],
 
267
                'repo': volume_info['repoName'],
 
268
                'lun': volume_info['lv']['lvStatus']['exportedLun']['lun'],
 
269
                'shelf': volume_info['lv']['lvStatus']['exportedLun']['shelf']}
 
270
 
 
271
    def get_volume_repository(self, volume_name):
 
272
        volume_info = self.get_volume_info(volume_name)
 
273
        return volume_info['repo']
 
274
 
 
275
    def get_all_repos(self):
 
276
        reply = self.rpc('fetch', {'orchStrRepo': ''}, None)
 
277
        try:
 
278
            return reply[0][1]['reply']
 
279
        except (IndexError, KeyError):
 
280
            return []
 
281
 
 
282
    def ping(self):
 
283
        try:
 
284
            self.rpc('fetch', {}, None, allow_empty_response=True)
 
285
        except Exception as e:
 
286
            LOG.debug(_('Coraid Appliance ping failed: %s'), str(e))
 
287
            raise exception.CoraidESMNotAvailable(reason=str(e))
 
288
 
 
289
    def create_lun(self, repository_name, volume_name, volume_size_in_gb):
 
290
        request = {'addr': 'cms',
 
291
                   'data': {
 
292
                       'servers': [],
 
293
                       'repoName': repository_name,
 
294
                       'lvName': volume_name,
 
295
                       'size': coraid_volume_size(volume_size_in_gb)},
 
296
                   'op': 'orchStrLun',
 
297
                   'args': 'add'}
 
298
        esm_result = self.esm_command(request)
 
299
        LOG.debug(_('Volume "%(name)s" created with VSX LUN "%(lun)s"') %
 
300
                  {'name': volume_name,
 
301
                   'lun': esm_result['firstParam']})
 
302
        return esm_result
228
303
 
229
304
    def delete_lun(self, volume_name):
230
 
        """Delete LUN."""
231
 
        try:
232
 
            volume_info = self._get_volume_info(volume_name)
233
 
            repository = volume_info['repo']
234
 
            data = '[{"addr":"cms","data":"{' \
235
 
                   '\\"repoName\\":\\"%(repo)s\\",' \
236
 
                   '\\"lvName\\":\\"%(volname)s\\"}",' \
237
 
                   '"op":"orchStrLun/verified",' \
238
 
                   '"args":"delete"}]' % dict(repo=repository,
239
 
                                              volname=volume_name)
240
 
            return self._configure(data)
241
 
        except Exception:
242
 
            if self._check_esm_alive():
243
 
                return True
244
 
            else:
245
 
                return False
 
305
        repository_name = self.get_volume_repository(volume_name)
 
306
        request = {'addr': 'cms',
 
307
                   'data': {
 
308
                       'repoName': repository_name,
 
309
                       'lvName': volume_name},
 
310
                   'op': 'orchStrLun/verified',
 
311
                   'args': 'delete'}
 
312
        esm_result = self.esm_command(request)
 
313
        LOG.debug(_('Volume "%s" deleted.'), volume_name)
 
314
        return esm_result
 
315
 
 
316
    def resize_volume(self, volume_name, new_volume_size_in_gb):
 
317
        LOG.debug(_('Resize volume "%(name)s" to %(size)s') %
 
318
                  {'name': volume_name,
 
319
                   'size': new_volume_size_in_gb})
 
320
        repository = self.get_volume_repository(volume_name)
 
321
        LOG.debug(_('Repository for volume "%(name)s" found: "%(repo)s"') %
 
322
                  {'name': volume_name,
 
323
                   'repo': repository})
 
324
 
 
325
        request = {'addr': 'cms',
 
326
                   'data': {
 
327
                       'lvName': volume_name,
 
328
                       'newLvName': volume_name + '-resize',
 
329
                       'size': coraid_volume_size(new_volume_size_in_gb),
 
330
                       'repoName': repository},
 
331
                   'op': 'orchStrLunMods',
 
332
                   'args': 'resize'}
 
333
        esm_result = self.esm_command(request)
 
334
 
 
335
        LOG.debug(_('Volume "%(name)s" resized. New size is %(size)s') %
 
336
                  {'name': volume_name,
 
337
                   'size': new_volume_size_in_gb})
 
338
        return esm_result
246
339
 
247
340
    def create_snapshot(self, volume_name, snapshot_name):
248
 
        """Create Snapshot."""
249
 
        volume_info = self._get_volume_info(volume_name)
250
 
        repository = volume_info['repo']
251
 
        data = '[{"addr":"cms","data":"{' \
252
 
               '\\"repoName\\":\\"%s\\",' \
253
 
               '\\"lvName\\":\\"%s\\",' \
254
 
               '\\"newLvName\\":\\"%s\\"}",' \
255
 
               '"op":"orchStrLunMods",' \
256
 
               '"args":"addClSnap"}]' % (repository, volume_name,
257
 
                                         snapshot_name)
258
 
        return self._configure(data)
 
341
        volume_repository = self.get_volume_repository(volume_name)
 
342
        request = {'addr': 'cms',
 
343
                   'data': {
 
344
                       'repoName': volume_repository,
 
345
                       'lvName': volume_name,
 
346
                       'newLvName': snapshot_name},
 
347
                   'op': 'orchStrLunMods',
 
348
                   'args': 'addClSnap'}
 
349
        esm_result = self.esm_command(request)
 
350
        return esm_result
259
351
 
260
352
    def delete_snapshot(self, snapshot_name):
261
 
        """Delete Snapshot."""
262
 
        snapshot_info = self._get_volume_info(snapshot_name)
263
 
        repository = snapshot_info['repo']
264
 
        data = '[{"addr":"cms","data":"{' \
265
 
               '\\"repoName\\":\\"%s\\",' \
266
 
               '\\"lvName\\":\\"%s\\"}",' \
267
 
               '"op":"orchStrLunMods",' \
268
 
               '"args":"delClSnap"}]' % (repository, snapshot_name)
269
 
        return self._configure(data)
270
 
 
271
 
    def create_volume_from_snapshot(self, snapshot_name,
272
 
                                    volume_name, repository):
273
 
        """Create a LUN from a Snapshot."""
274
 
        snapshot_info = self._get_volume_info(snapshot_name)
275
 
        snapshot_repo = snapshot_info['repo']
276
 
        data = '[{"addr":"cms","data":"{' \
277
 
               '\\"lvName\\":\\"%s\\",' \
278
 
               '\\"repoName\\":\\"%s\\",' \
279
 
               '\\"newLvName\\":\\"%s\\",' \
280
 
               '\\"newRepoName\\":\\"%s\\"}",' \
281
 
               '"op":"orchStrLunMods",' \
282
 
               '"args":"addClone"}]' % (snapshot_name, snapshot_repo,
283
 
                                        volume_name, repository)
284
 
        return self._configure(data)
285
 
 
286
 
    def resize_volume(self, volume_name, volume_size):
287
 
        volume_info = self._get_volume_info(volume_name)
288
 
        repository = volume_info['repo']
289
 
        data = '[{"addr":"cms","data":"{' \
290
 
               '\\"lvName\\":\\"%s\\",' \
291
 
               '\\"newLvSize\\":\\"%s\\"}",' \
292
 
               '\\"repoName\\":\\"%s\\"}",' \
293
 
               '"op":"orchStrLunMods",' \
294
 
               '"args":"resizeVolume"}]' % (volume_name,
295
 
                                            volume_size,
296
 
                                            repository)
297
 
        return self._configure(data)
 
353
        repository_name = self.get_volume_repository(snapshot_name)
 
354
        request = {'addr': 'cms',
 
355
                   'data': {
 
356
                       'repoName': repository_name,
 
357
                       'lvName': snapshot_name},
 
358
                   'op': 'orchStrLunMods',
 
359
                   'args': 'delClSnap'}
 
360
        esm_result = self.esm_command(request)
 
361
        return esm_result
 
362
 
 
363
    def create_volume_from_snapshot(self,
 
364
                                    snapshot_name,
 
365
                                    volume_name,
 
366
                                    dest_repository_name):
 
367
        snapshot_repo = self.get_volume_repository(snapshot_name)
 
368
        request = {'addr': 'cms',
 
369
                   'data': {
 
370
                       'lvName': snapshot_name,
 
371
                       'repoName': snapshot_repo,
 
372
                       'newLvName': volume_name,
 
373
                       'newRepoName': dest_repository_name},
 
374
                   'op': 'orchStrLunMods',
 
375
                   'args': 'addClone'}
 
376
        esm_result = self.esm_command(request)
 
377
        return esm_result
 
378
 
 
379
    def clone_volume(self,
 
380
                     src_volume_name,
 
381
                     dst_volume_name,
 
382
                     dst_repository_name):
 
383
        src_volume_info = self.get_volume_info(src_volume_name)
 
384
 
 
385
        if src_volume_info['repo'] != dst_repository_name:
 
386
            raise exception.CoraidException(
 
387
                _('Cannot create clone volume in different repository.'))
 
388
 
 
389
        request = {'addr': 'cms',
 
390
                   'data': {
 
391
                       'shelfLun': '{0}.{1}'.format(src_volume_info['shelf'],
 
392
                                                    src_volume_info['lun']),
 
393
                       'lvName': src_volume_name,
 
394
                       'repoName': src_volume_info['repo'],
 
395
                       'newLvName': dst_volume_name,
 
396
                       'newRepoName': dst_repository_name},
 
397
                   'op': 'orchStrLunMods',
 
398
                   'args': 'addClone'}
 
399
        return self.esm_command(request)
298
400
 
299
401
 
300
402
class CoraidDriver(driver.VolumeDriver):
301
403
    """This is the Class to set in cinder.conf (volume_driver)."""
302
404
 
 
405
    VERSION = '1.0.0'
 
406
 
303
407
    def __init__(self, *args, **kwargs):
304
408
        super(CoraidDriver, self).__init__(*args, **kwargs)
305
409
        self.configuration.append_config_values(coraid_opts)
306
410
 
307
 
    def do_setup(self, context):
308
 
        """Initialize the volume driver."""
309
 
        self.esm = CoraidRESTClient(self.configuration.coraid_esm_address,
310
 
                                    self.configuration.coraid_user,
311
 
                                    self.configuration.coraid_group,
312
 
                                    self.configuration.coraid_password)
 
411
        self._stats = {'driver_version': self.VERSION,
 
412
                       'free_capacity_gb': 'unknown',
 
413
                       'reserved_percentage': 0,
 
414
                       'storage_protocol': 'aoe',
 
415
                       'total_capacity_gb': 'unknown',
 
416
                       'vendor_name': 'Coraid'}
 
417
        backend_name = self.configuration.safe_get('volume_backend_name')
 
418
        self._stats['volume_backend_name'] = backend_name or 'EtherCloud ESM'
 
419
 
 
420
    @property
 
421
    def appliance(self):
 
422
        # NOTE(nsobolevsky): This is workaround for bug in the ESM appliance.
 
423
        # If there is a lot of request with the same session/cookie/connection,
 
424
        # the appliance could corrupt all following request in session.
 
425
        # For that purpose we just create a new appliance.
 
426
        esm_url = "https://{0}:8443".format(
 
427
            self.configuration.coraid_esm_address)
 
428
 
 
429
        return CoraidAppliance(CoraidRESTClient(esm_url),
 
430
                               self.configuration.coraid_user,
 
431
                               self.configuration.coraid_password,
 
432
                               self.configuration.coraid_group)
313
433
 
314
434
    def check_for_setup_error(self):
315
435
        """Return an error if prerequisites aren't met."""
316
 
        if not self.esm._login():
317
 
            raise LookupError(_("Cannot login on Coraid ESM"))
 
436
        self.appliance.ping()
318
437
 
319
438
    def _get_repository(self, volume_type):
320
 
        """
321
 
        Return the ESM Repository from the Volume Type.
 
439
        """Get the ESM Repository from the Volume Type.
 
440
 
322
441
        The ESM Repository is stored into a volume_type_extra_specs key.
323
442
        """
324
443
        volume_type_id = volume_type['id']
325
444
        repository_key_name = self.configuration.coraid_repository_key
326
445
        repository = volume_types.get_volume_type_extra_specs(
327
446
            volume_type_id, repository_key_name)
328
 
        return repository
 
447
        # Remove <in> keyword from repository name if needed
 
448
        if repository.startswith('<in> '):
 
449
            return repository[len('<in> '):]
 
450
        else:
 
451
            return repository
329
452
 
330
453
    def create_volume(self, volume):
331
454
        """Create a Volume."""
332
 
        try:
333
 
            repository = self._get_repository(volume['volume_type'])
334
 
            self.esm.create_lun(volume['name'], volume['size'], repository)
335
 
        except Exception:
336
 
            msg = _('Fail to create volume %(volname)s')
337
 
            LOG.debug(msg % dict(volname=volume['name']))
338
 
            raise
339
 
        # NOTE(jbr_): The manager currently interprets any return as
340
 
        # being the model_update for provider location.
341
 
        # return None to not break it (thank to jgriffith and DuncanT)
342
 
        return
 
455
        repository = self._get_repository(volume['volume_type'])
 
456
        self.appliance.create_lun(repository, volume['name'], volume['size'])
 
457
 
 
458
    def create_cloned_volume(self, volume, src_vref):
 
459
        dst_volume_repository = self._get_repository(volume['volume_type'])
 
460
 
 
461
        self.appliance.clone_volume(src_vref['name'],
 
462
                                    volume['name'],
 
463
                                    dst_volume_repository)
 
464
 
 
465
        if volume['size'] != src_vref['size']:
 
466
            self.appliance.resize_volume(volume['name'], volume['size'])
343
467
 
344
468
    def delete_volume(self, volume):
345
469
        """Delete a Volume."""
346
470
        try:
347
 
            self.esm.delete_lun(volume['name'])
348
 
        except Exception:
349
 
            msg = _('Failed to delete volume %(volname)s')
350
 
            LOG.debug(msg % dict(volname=volume['name']))
351
 
            raise
352
 
        return
 
471
            self.appliance.delete_lun(volume['name'])
 
472
        except exception.VolumeNotFound:
 
473
            self.appliance.ping()
353
474
 
354
475
    def create_snapshot(self, snapshot):
355
476
        """Create a Snapshot."""
356
 
        volume_name = (self.configuration.volume_name_template
357
 
                       % snapshot['volume_id'])
358
 
        snapshot_name = (self.configuration.snapshot_name_template
359
 
                         % snapshot['id'])
360
 
        try:
361
 
            self.esm.create_snapshot(volume_name, snapshot_name)
362
 
        except Exception as e:
363
 
            msg = _('Failed to Create Snapshot %(snapname)s')
364
 
            LOG.debug(msg % dict(snapname=snapshot_name))
365
 
            raise
366
 
        return
 
477
        volume_name = snapshot['volume_name']
 
478
        snapshot_name = snapshot['name']
 
479
        self.appliance.create_snapshot(volume_name, snapshot_name)
367
480
 
368
481
    def delete_snapshot(self, snapshot):
369
482
        """Delete a Snapshot."""
370
 
        snapshot_name = (self.configuration.snapshot_name_template
371
 
                         % snapshot['id'])
372
 
        try:
373
 
            self.esm.delete_snapshot(snapshot_name)
374
 
        except Exception:
375
 
            msg = _('Failed to Delete Snapshot %(snapname)s')
376
 
            LOG.debug(msg % dict(snapname=snapshot_name))
377
 
            raise
378
 
        return
 
483
        snapshot_name = snapshot['name']
 
484
        self.appliance.delete_snapshot(snapshot_name)
379
485
 
380
486
    def create_volume_from_snapshot(self, volume, snapshot):
381
487
        """Create a Volume from a Snapshot."""
382
 
        snapshot_name = (self.configuration.snapshot_name_template
383
 
                         % snapshot['id'])
 
488
        snapshot_name = snapshot['name']
384
489
        repository = self._get_repository(volume['volume_type'])
385
 
        try:
386
 
            self.esm.create_volume_from_snapshot(snapshot_name,
387
 
                                                 volume['name'],
388
 
                                                 repository)
389
 
            resize = volume['size'] > snapshot['volume_size']
390
 
            if resize:
391
 
                self.esm.resize_volume(volume['name'], volume['size'])
392
 
        except Exception:
393
 
            msg = _('Failed to Create Volume from Snapshot %(snapname)s')
394
 
            LOG.debug(msg % dict(snapname=snapshot_name))
395
 
            raise
396
 
        return
 
490
        self.appliance.create_volume_from_snapshot(snapshot_name,
 
491
                                                   volume['name'],
 
492
                                                   repository)
 
493
        if volume['size'] > snapshot['volume_size']:
 
494
            self.appliance.resize_volume(volume['name'], volume['size'])
397
495
 
398
496
    def extend_volume(self, volume, new_size):
399
 
        """Extend an Existing Volume."""
400
 
        try:
401
 
            self.esm.resize_volume(volume['name'], new_size)
402
 
        except Exception:
403
 
            msg = _('Failed to Extend Volume %(volname)s')
404
 
            LOG.debug(msg % dict(volname=volume['name']))
405
 
            raise
406
 
        return
 
497
        """Extend an existing volume."""
 
498
        self.appliance.resize_volume(volume['name'], new_size)
407
499
 
408
500
    def initialize_connection(self, volume, connector):
409
501
        """Return connection information."""
410
 
        try:
411
 
            infos = self.esm._get_lun_address(volume['name'])
412
 
            shelf = infos['shelf']
413
 
            lun = infos['lun']
414
 
 
415
 
            aoe_properties = {
416
 
                'target_shelf': shelf,
417
 
                'target_lun': lun,
418
 
            }
419
 
            return {
420
 
                'driver_volume_type': 'aoe',
421
 
                'data': aoe_properties,
422
 
            }
423
 
        except Exception:
424
 
            msg = _('Failed to Initialize Connection. '
425
 
                    'Volume Name: %(volname)s '
426
 
                    'Shelf: %(shelf)s, '
427
 
                    'Lun: %(lun)s')
428
 
            LOG.debug(msg % dict(volname=volume['name'],
429
 
                                 shelf=shelf,
430
 
                                 lun=lun))
431
 
            raise
432
 
        return
 
502
        volume_info = self.appliance.get_volume_info(volume['name'])
 
503
 
 
504
        shelf = volume_info['shelf']
 
505
        lun = volume_info['lun']
 
506
 
 
507
        LOG.debug(_('Initialize connection %(shelf)s/%(lun)s for %(name)s') %
 
508
                  {'shelf': shelf,
 
509
                   'lun': lun,
 
510
                   'name': volume['name']})
 
511
 
 
512
        aoe_properties = {'target_shelf': shelf,
 
513
                          'target_lun': lun}
 
514
 
 
515
        return {'driver_volume_type': 'aoe',
 
516
                'data': aoe_properties}
 
517
 
 
518
    def _get_repository_capabilities(self):
 
519
        repos_list = map(lambda i: i['profile']['fullName'] + ':' + i['name'],
 
520
                         self.appliance.get_all_repos())
 
521
        return ' '.join(repos_list)
 
522
 
 
523
    def update_volume_stats(self):
 
524
        capabilities = self._get_repository_capabilities()
 
525
        self._stats[self.configuration.coraid_repository_key] = capabilities
433
526
 
434
527
    def get_volume_stats(self, refresh=False):
435
528
        """Return Volume Stats."""
436
 
        data = {'driver_version': '1.0',
437
 
                'free_capacity_gb': 'unknown',
438
 
                'reserved_percentage': 0,
439
 
                'storage_protocol': 'aoe',
440
 
                'total_capacity_gb': 'unknown',
441
 
                'vendor_name': 'Coraid'}
442
 
        backend_name = self.configuration.safe_get('volume_backend_name')
443
 
        data['volume_backend_name'] = backend_name or 'EtherCloud ESM'
444
 
        return data
 
529
        if refresh:
 
530
            self.update_volume_stats()
 
531
        return self._stats
445
532
 
446
533
    def local_path(self, volume):
447
534
        pass
457
544
 
458
545
    def ensure_export(self, context, volume):
459
546
        pass
460
 
 
461
 
    def detach_volume(self, context, volume):
462
 
        pass