components/openstack/cinder/files/zfssa/restclient.py
branchs11-update
changeset 3178 77584387a894
equal deleted inserted replaced
3175:1ff833d174d4 3178:77584387a894
       
     1 # Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
       
     2 #
       
     3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
       
     4 #    not use this file except in compliance with the License. You may obtain
       
     5 #    a copy of the License at
       
     6 #
       
     7 #         http://www.apache.org/licenses/LICENSE-2.0
       
     8 #
       
     9 #    Unless required by applicable law or agreed to in writing, software
       
    10 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
       
    11 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
       
    12 #    License for the specific language governing permissions and limitations
       
    13 #    under the License.
       
    14 """
       
    15 ZFS Storage Appliance REST API Client Programmatic Interface
       
    16 """
       
    17 
       
    18 import httplib
       
    19 import json
       
    20 import time
       
    21 import urllib2
       
    22 import StringIO
       
    23 
       
    24 from cinder.openstack.common import log
       
    25 
       
    26 LOG = log.getLogger(__name__)
       
    27 
       
    28 
       
    29 class Status:
       
    30     """Result HTTP Status"""
       
    31 
       
    32     def __init__(self):
       
    33         pass
       
    34 
       
    35     #: Request return OK
       
    36     OK = httplib.OK
       
    37 
       
    38     #: New resource created successfully
       
    39     CREATED = httplib.CREATED
       
    40 
       
    41     #: Command accepted
       
    42     ACCEPTED = httplib.ACCEPTED
       
    43 
       
    44     #: Command returned OK but no data will be returned
       
    45     NO_CONTENT = httplib.NO_CONTENT
       
    46 
       
    47     #: Bad Request
       
    48     BAD_REQUEST = httplib.BAD_REQUEST
       
    49 
       
    50     #: User is not authorized
       
    51     UNAUTHORIZED = httplib.UNAUTHORIZED
       
    52 
       
    53     #: The request is not allowed
       
    54     FORBIDDEN = httplib.FORBIDDEN
       
    55 
       
    56     #: The requested resource was not found
       
    57     NOT_FOUND = httplib.NOT_FOUND
       
    58 
       
    59     #: The request is not allowed
       
    60     NOT_ALLOWED = httplib.METHOD_NOT_ALLOWED
       
    61 
       
    62     #: Request timed out
       
    63     TIMEOUT = httplib.REQUEST_TIMEOUT
       
    64 
       
    65     #: Invalid request
       
    66     CONFLICT = httplib.CONFLICT
       
    67 
       
    68     #: Service Unavailable
       
    69     BUSY = httplib.SERVICE_UNAVAILABLE
       
    70 
       
    71 
       
    72 class RestResult(object):
       
    73     """Result from a REST API operation"""
       
    74     def __init__(self, response=None, err=None):
       
    75         """Initialize a RestResult containing the results from a REST call
       
    76         :param response: HTTP response
       
    77         """
       
    78         self.response = response
       
    79         self.error = err
       
    80         self.data = ""
       
    81         self.status = 0
       
    82         if self.response is not None:
       
    83             self.status = self.response.getcode()
       
    84             result = self.response.read()
       
    85             while result:
       
    86                 self.data += result
       
    87                 result = self.response.read()
       
    88 
       
    89         if self.error is not None:
       
    90             self.status = self.error.code
       
    91             self.data = httplib.responses[self.status]
       
    92 
       
    93         LOG.debug('response code: %s' % self.status)
       
    94         LOG.debug('response data: %s' % self.data)
       
    95 
       
    96     def get_header(self, name):
       
    97         """Get an HTTP header with the given name from the results
       
    98 
       
    99         :param name: HTTP header name
       
   100         :return: The header value or None if no value is found
       
   101         """
       
   102         if self.response is None:
       
   103             return None
       
   104         info = self.response.info()
       
   105         return info.getheader(name)
       
   106 
       
   107 
       
   108 class RestClientError(Exception):
       
   109     """Exception for ZFS REST API client errors"""
       
   110     def __init__(self, status, name="ERR_INTERNAL", message=None):
       
   111 
       
   112         """Create a REST Response exception
       
   113 
       
   114         :param status: HTTP response status
       
   115         :param name: The name of the REST API error type
       
   116         :param message: Descriptive error message returned from REST call
       
   117         """
       
   118         Exception.__init__(self, message)
       
   119         self.code = status
       
   120         self.name = name
       
   121         self.msg = message
       
   122         if status in httplib.responses:
       
   123             self.msg = httplib.responses[status]
       
   124 
       
   125     def __str__(self):
       
   126         return "%d %s %s" % (self.code, self.name, self.msg)
       
   127 
       
   128 
       
   129 class RestClientURL(object):
       
   130     """ZFSSA urllib2 client"""
       
   131     def __init__(self, url, **kwargs):
       
   132         """
       
   133         Initialize a REST client.
       
   134 
       
   135         :param url: The ZFSSA REST API URL
       
   136         :key session: HTTP Cookie value of x-auth-session obtained from a
       
   137                       normal BUI login.
       
   138         :key timeout: Time in seconds to wait for command to complete.
       
   139                       (Default is 60 seconds)
       
   140         """
       
   141         self.url = url
       
   142         self.local = kwargs.get("local", False)
       
   143         self.base_path = kwargs.get("base_path", "/api")
       
   144         self.timeout = kwargs.get("timeout", 60)
       
   145         self.headers = None
       
   146         if kwargs.get('session'):
       
   147             self.headers['x-auth-session'] = kwargs.get('session')
       
   148 
       
   149         self.headers = {"content-type": "application/json"}
       
   150         self.do_logout = False
       
   151         self.auth_str = None
       
   152 
       
   153     def _path(self, path, base_path=None):
       
   154         """build rest url path"""
       
   155         if path.startswith("http://") or path.startswith("https://"):
       
   156             return path
       
   157         if base_path is None:
       
   158             base_path = self.base_path
       
   159         if not path.startswith(base_path) and not (
       
   160                 self.local and ("/api" + path).startswith(base_path)):
       
   161             path = "%s%s" % (base_path, path)
       
   162         if self.local and path.startswith("/api"):
       
   163             path = path[4:]
       
   164         return self.url + path
       
   165 
       
   166     def authorize(self):
       
   167         """Performs authorization setting x-auth-session"""
       
   168         self.headers['authorization'] = 'Basic %s' % self.auth_str
       
   169         if 'x-auth-session' in self.headers:
       
   170             del self.headers['x-auth-session']
       
   171 
       
   172         try:
       
   173             result = self.post("/access/v1")
       
   174             del self.headers['authorization']
       
   175             if result.status == httplib.CREATED:
       
   176                 self.headers['x-auth-session'] = \
       
   177                     result.get_header('x-auth-session')
       
   178                 self.do_logout = True
       
   179                 LOG.info('ZFSSA version: %s' %
       
   180                          result.get_header('x-zfssa-version'))
       
   181 
       
   182             elif result.status == httplib.NOT_FOUND:
       
   183                 raise RestClientError(result.status, name="ERR_RESTError",
       
   184                                       message="REST Not Available: \
       
   185                                       Please Upgrade")
       
   186 
       
   187         except RestClientError as err:
       
   188             del self.headers['authorization']
       
   189             raise err
       
   190 
       
   191     def login(self, auth_str):
       
   192         """
       
   193         Login to an appliance using a user name and password and start
       
   194         a session like what is done logging into the BUI.  This is not a
       
   195         requirement to run REST commands, since the protocol is stateless.
       
   196         What is does is set up a cookie session so that some server side
       
   197         caching can be done.  If login is used remember to call logout when
       
   198         finished.
       
   199 
       
   200         :param auth_str: Authorization string (base64)
       
   201         """
       
   202         self.auth_str = auth_str
       
   203         self.authorize()
       
   204 
       
   205     def logout(self):
       
   206         """Logout of an appliance"""
       
   207         result = None
       
   208         try:
       
   209             result = self.delete("/access/v1", base_path="/api")
       
   210         except RestClientError:
       
   211             pass
       
   212 
       
   213         self.headers.clear()
       
   214         self.do_logout = False
       
   215         return result
       
   216 
       
   217     def islogin(self):
       
   218         """return if client is login"""
       
   219         return self.do_logout
       
   220 
       
   221     @staticmethod
       
   222     def mkpath(*args, **kwargs):
       
   223         """Make a path?query string for making a REST request
       
   224 
       
   225         :cmd_params args: The path part
       
   226         :cmd_params kwargs: The query part
       
   227         """
       
   228         buf = StringIO()
       
   229         query = "?"
       
   230         for arg in args:
       
   231             buf.write("/")
       
   232             buf.write(arg)
       
   233         for k in kwargs:
       
   234             buf.write(query)
       
   235             if query == "?":
       
   236                 query = "&"
       
   237             buf.write(k)
       
   238             buf.write("=")
       
   239             buf.write(kwargs[k])
       
   240         return buf.getvalue()
       
   241 
       
   242     def request(self, path, request, body=None, **kwargs):
       
   243         """Make an HTTP request and return the results
       
   244 
       
   245         :param path: Path used with the initiazed URL to make a request
       
   246         :param request: HTTP request type (GET, POST, PUT, DELETE)
       
   247         :param body: HTTP body of request
       
   248         :key accept: Set HTTP 'Accept' header with this value
       
   249         :key base_path: Override the base_path for this request
       
   250         :key content: Set HTTP 'Content-Type' header with this value
       
   251         """
       
   252         out_hdrs = dict.copy(self.headers)
       
   253         if kwargs.get("accept"):
       
   254             out_hdrs['accept'] = kwargs.get("accept")
       
   255 
       
   256         if body is not None:
       
   257             if isinstance(body, dict):
       
   258                 body = str(json.dumps(body))
       
   259 
       
   260         if body and len(body):
       
   261             out_hdrs['content-length'] = len(body)
       
   262 
       
   263         zfssaurl = self._path(path, kwargs.get("base_path"))
       
   264         req = urllib2.Request(zfssaurl, body, out_hdrs)
       
   265         req.get_method = lambda: request
       
   266         maxreqretries = kwargs.get("maxreqretries", 10)
       
   267         retry = 0
       
   268         response = None
       
   269 
       
   270         LOG.debug('request: %s %s' % (request, zfssaurl))
       
   271         LOG.debug('out headers: %s' % out_hdrs)
       
   272         if body is not None and body != '':
       
   273             LOG.debug('body: %s' % body)
       
   274 
       
   275         while retry < maxreqretries:
       
   276             try:
       
   277                 response = urllib2.urlopen(req, timeout=self.timeout)
       
   278             except urllib2.HTTPError as err:
       
   279                 LOG.error('REST Not Available: %s' % err.code)
       
   280                 if err.code == httplib.SERVICE_UNAVAILABLE and \
       
   281                    retry < maxreqretries:
       
   282                     retry += 1
       
   283                     time.sleep(1)
       
   284                     LOG.error('Server Busy retry request: %s' % retry)
       
   285                     continue
       
   286                 if (err.code == httplib.UNAUTHORIZED or
       
   287                     err.code == httplib.INTERNAL_SERVER_ERROR) and \
       
   288                    '/access/v1' not in zfssaurl:
       
   289                     try:
       
   290                         LOG.error('Authorizing request retry: %s, %s' %
       
   291                                   (zfssaurl, retry))
       
   292                         self.authorize()
       
   293                         req.add_header('x-auth-session',
       
   294                                        self.headers['x-auth-session'])
       
   295                     except RestClientError:
       
   296                         pass
       
   297                     retry += 1
       
   298                     time.sleep(1)
       
   299                     continue
       
   300 
       
   301                 return RestResult(err=err)
       
   302 
       
   303             except urllib2.URLError as err:
       
   304                 LOG.error('URLError: %s' % err.reason)
       
   305                 raise RestClientError(-1, name="ERR_URLError",
       
   306                                       message=err.reason)
       
   307 
       
   308             break
       
   309 
       
   310         if response and response.getcode() == httplib.SERVICE_UNAVAILABLE and \
       
   311            retry >= maxreqretries:
       
   312             raise RestClientError(response.getcode(), name="ERR_HTTPError",
       
   313                                   message="REST Not Available: Disabled")
       
   314 
       
   315         return RestResult(response=response)
       
   316 
       
   317     def get(self, path, **kwargs):
       
   318         """
       
   319         Make an HTTP GET request
       
   320 
       
   321         :param path: Path to resource.
       
   322         """
       
   323         return self.request(path, "GET", **kwargs)
       
   324 
       
   325     def post(self, path, body="", **kwargs):
       
   326         """Make an HTTP POST request
       
   327 
       
   328         :param path: Path to resource.
       
   329         :param body: Post data content
       
   330         """
       
   331         return self.request(path, "POST", body, **kwargs)
       
   332 
       
   333     def put(self, path, body="", **kwargs):
       
   334         """Make an HTTP PUT request
       
   335 
       
   336         :param path: Path to resource.
       
   337         :param body: Put data content
       
   338         """
       
   339         return self.request(path, "PUT", body, **kwargs)
       
   340 
       
   341     def delete(self, path, **kwargs):
       
   342         """Make an HTTP DELETE request
       
   343 
       
   344         :param path: Path to resource that will be deleted.
       
   345         """
       
   346         return self.request(path, "DELETE", **kwargs)
       
   347 
       
   348     def head(self, path, **kwargs):
       
   349         """Make an HTTP HEAD request
       
   350 
       
   351         :param path: Path to resource.
       
   352         """
       
   353         return self.request(path, "HEAD", **kwargs)