# 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)