summaryrefslogtreecommitdiff
path: root/manila/share/drivers/zfssa/restclient.py
blob: df1bc14a247ae051228541d3998267b17926e671 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
"""
ZFS Storage Appliance REST API Client Programmatic Interface
TODO(diemtran): this module needs to be placed in a library common to OpenStack
    services. When this happens, the file should be removed from Manila code
    base and imported from the relevant library.
"""

import time

from oslo_serialization import jsonutils
import six
from six.moves import http_client
# pylint: disable=E0611,F0401
from six.moves.urllib import error as urlerror
from six.moves.urllib import request as urlrequest


def log_debug_msg(obj, message):
    if obj.log_function:
        obj.log_function(message)


class Status(object):
    """Result HTTP Status."""

    #: Request return OK
    OK = http_client.OK  # pylint: disable=invalid-name

    #: New resource created successfully
    CREATED = http_client.CREATED

    #: Command accepted
    ACCEPTED = http_client.ACCEPTED

    #: Command returned OK but no data will be returned
    NO_CONTENT = http_client.NO_CONTENT

    #: Bad Request
    BAD_REQUEST = http_client.BAD_REQUEST

    #: User is not authorized
    UNAUTHORIZED = http_client.UNAUTHORIZED

    #: The request is not allowed
    FORBIDDEN = http_client.FORBIDDEN

    #: The requested resource was not found
    NOT_FOUND = http_client.NOT_FOUND

    #: The request is not allowed
    NOT_ALLOWED = http_client.METHOD_NOT_ALLOWED

    #: Request timed out
    TIMEOUT = http_client.REQUEST_TIMEOUT

    #: Invalid request
    CONFLICT = http_client.CONFLICT

    #: Service Unavailable
    BUSY = http_client.SERVICE_UNAVAILABLE


class RestResult(object):
    """Result from a REST API operation."""
    def __init__(self, logfunc=None, response=None, err=None):
        """Initialize a RestResult containing the results from a REST call.

        :param logfunc: debug log function.
        :param response: HTTP response.
        :param err: HTTP error.
        """
        self.response = response
        self.log_function = logfunc
        self.error = err
        self.data = ""
        self.status = 0
        if self.response:
            self.status = self.response.getcode()
            result = self.response.read()
            while result:
                self.data += result
                result = self.response.read()

        if self.error:
            self.status = self.error.code
            self.data = http_client.responses[self.status]

        log_debug_msg(self, 'Response code: %s' % self.status)
        log_debug_msg(self, 'Response data: %s' % self.data)

    def get_header(self, name):
        """Get an HTTP header with the given name from the results.

        :param name: HTTP header name.
        :return: The header value or None if no value is found.
        """
        if self.response is None:
            return None
        info = self.response.info()
        return info.getheader(name)


class RestClientError(Exception):
    """Exception for ZFS REST API client errors."""
    def __init__(self, status, name="ERR_INTERNAL", message=None):

        """Create a REST Response exception.

        :param status: HTTP response status.
        :param name: The name of the REST API error type.
        :param message: Descriptive error message returned from REST call.
        """
        super(RestClientError, self).__init__(message)
        self.code = status
        self.name = name
        self.msg = message
        if status in http_client.responses:
            self.msg = http_client.responses[status]

    def __str__(self):
        return "%d %s %s" % (self.code, self.name, self.msg)


class RestClientURL(object):  # pylint: disable=R0902
    """ZFSSA urllib client."""
    def __init__(self, url, logfunc=None, **kwargs):
        """Initialize a REST client.

        :param url: The ZFSSA REST API URL.
        :key session: HTTP Cookie value of x-auth-session obtained from a
                      normal BUI login.
        :key timeout: Time in seconds to wait for command to complete.
                      (Default is 60 seconds).
        """
        self.url = url
        self.log_function = logfunc
        self.local = kwargs.get("local", False)
        self.base_path = kwargs.get("base_path", "/api")
        self.timeout = kwargs.get("timeout", 60)
        self.headers = None
        if kwargs.get('session'):
            self.headers['x-auth-session'] = kwargs.get('session')

        self.headers = {"content-type": "application/json"}
        self.do_logout = False
        self.auth_str = None

    def _path(self, path, base_path=None):
        """Build rest url path."""
        if path.startswith("http://") or path.startswith("https://"):
            return path
        if base_path is None:
            base_path = self.base_path
        if not path.startswith(base_path) and not (
                self.local and ("/api" + path).startswith(base_path)):
            path = "%s%s" % (base_path, path)
        if self.local and path.startswith("/api"):
            path = path[4:]
        return self.url + path

    def _authorize(self):
        """Performs authorization setting x-auth-session."""
        self.headers['authorization'] = 'Basic %s' % self.auth_str
        if 'x-auth-session' in self.headers:
            del self.headers['x-auth-session']

        try:
            result = self.post("/access/v1")
            del self.headers['authorization']
            if result.status == http_client.CREATED:
                self.headers['x-auth-session'] = (
                    result.get_header('x-auth-session'))
                self.do_logout = True
                log_debug_msg(self, ('ZFSSA version: %s')
                              % result.get_header('x-zfssa-version'))

            elif result.status == http_client.NOT_FOUND:
                raise RestClientError(result.status, name="ERR_RESTError",
                                      message=("REST Not Available:"
                                               "Please Upgrade"))

        except RestClientError:
            del self.headers['authorization']
            raise

    def login(self, auth_str):
        """Login to an appliance using a user name and password.

        Start a session like what is done logging into the BUI.  This is not a
        requirement to run REST commands, since the protocol is stateless.
        What is does is set up a cookie session so that some server side
        caching can be done.  If login is used remember to call logout when
        finished.

        :param auth_str: Authorization string (base64).
        """
        self.auth_str = auth_str
        self._authorize()

    def logout(self):
        """Logout of an appliance."""
        result = None
        try:
            result = self.delete("/access/v1", base_path="/api")
        except RestClientError:
            pass

        self.headers.clear()
        self.do_logout = False
        return result

    def islogin(self):
        """return if client is login."""
        return self.do_logout

    @staticmethod
    def mkpath(*args, **kwargs):
        """Make a path?query string for making a REST request.

        :cmd_params args: The path part.
        :cmd_params kwargs: The query part.
        """
        buf = six.StringIO()
        query = "?"
        for arg in args:
            buf.write("/")
            buf.write(arg)
        for k in kwargs:
            buf.write(query)
            if query == "?":
                query = "&"
            buf.write(k)
            buf.write("=")
            buf.write(kwargs[k])
        return buf.getvalue()

    # pylint: disable=R0912
    def request(self, path, request, body=None, **kwargs):
        """Make an HTTP request and return the results.

        :param path: Path used with the initialized URL to make a request.
        :param request: HTTP request type (GET, POST, PUT, DELETE).
        :param body: HTTP body of request.
        :key accept: Set HTTP 'Accept' header with this value.
        :key base_path: Override the base_path for this request.
        :key content: Set HTTP 'Content-Type' header with this value.
        """
        out_hdrs = dict.copy(self.headers)
        if kwargs.get("accept"):
            out_hdrs['accept'] = kwargs.get("accept")

        if body:
            if isinstance(body, dict):
                body = six.text_type(jsonutils.dumps(body))

        if body and len(body):
            out_hdrs['content-length'] = len(body)

        zfssaurl = self._path(path, kwargs.get("base_path"))
        req = urlrequest.Request(zfssaurl, body, out_hdrs)
        req.get_method = lambda: request
        maxreqretries = kwargs.get("maxreqretries", 10)
        retry = 0
        response = None

        log_debug_msg(self, 'Request: %s %s' % (request, zfssaurl))
        log_debug_msg(self, 'Out headers: %s' % out_hdrs)
        if body and body != '':
            log_debug_msg(self, 'Body: %s' % body)

        while retry < maxreqretries:
            try:
                response = urlrequest.urlopen(req, timeout=self.timeout)
            except urlerror.HTTPError as err:
                if err.code == http_client.NOT_FOUND:
                    log_debug_msg(self, 'REST Not Found: %s' % err.code)
                else:
                    log_debug_msg(self, ('REST Not Available: %s') % err.code)

                if (err.code == http_client.SERVICE_UNAVAILABLE and
                        retry < maxreqretries):
                    retry += 1
                    time.sleep(1)
                    log_debug_msg(self, ('Server Busy retry request: %s')
                                  % retry)
                    continue
                if ((err.code == http_client.UNAUTHORIZED or
                     err.code == http_client.INTERNAL_SERVER_ERROR) and
                        '/access/v1' not in zfssaurl):
                    try:
                        log_debug_msg(self, ('Authorizing request: '
                                             '%(zfssaurl)s'
                                             'retry: %(retry)d .')
                                      % {'zfssaurl': zfssaurl,
                                         'retry': retry})
                        self._authorize()
                        req.add_header('x-auth-session',
                                       self.headers['x-auth-session'])
                    except RestClientError:
                        log_debug_msg(self, ('Cannot authorize.'))
                    retry += 1
                    time.sleep(1)
                    continue

                return RestResult(self.log_function, err=err)

            except urlerror.URLError as err:
                log_debug_msg(self, ('URLError: %s') % err.reason)
                raise RestClientError(-1, name="ERR_URLError",
                                      message=err.reason)
            break

        if ((response and
             response.getcode() == http_client.SERVICE_UNAVAILABLE) and
                retry >= maxreqretries):
            raise RestClientError(response.getcode(), name="ERR_HTTPError",
                                  message="REST Not Available: Disabled")

        return RestResult(self.log_function, response=response)

    def get(self, path, **kwargs):
        """Make an HTTP GET request.

        :param path: Path to resource.
        """
        return self.request(path, "GET", **kwargs)

    def post(self, path, body="", **kwargs):
        """Make an HTTP POST request.

        :param path: Path to resource.
        :param body: Post data content.
        """
        return self.request(path, "POST", body, **kwargs)

    def put(self, path, body="", **kwargs):
        """Make an HTTP PUT request.

        :param path: Path to resource.
        :param body: Put data content.
        """
        return self.request(path, "PUT", body, **kwargs)

    def delete(self, path, **kwargs):
        """Make an HTTP DELETE request.

        :param path: Path to resource that will be deleted.
        """
        return self.request(path, "DELETE", **kwargs)

    def head(self, path, **kwargs):
        """Make an HTTP HEAD request.

        :param path: Path to resource.
        """
        return self.request(path, "HEAD", **kwargs)