--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/components/openstack/cinder/files/zfssa/restclient.py Fri Jun 13 09:10:23 2014 -0700
@@ -0,0 +1,353 @@
+# 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
+"""
+
+import httplib
+import json
+import time
+import urllib2
+import StringIO
+
+from cinder.openstack.common import log
+
+LOG = log.getLogger(__name__)
+
+
+class Status:
+ """Result HTTP Status"""
+
+ def __init__(self):
+ pass
+
+ #: Request return OK
+ OK = httplib.OK
+
+ #: New resource created successfully
+ CREATED = httplib.CREATED
+
+ #: Command accepted
+ ACCEPTED = httplib.ACCEPTED
+
+ #: Command returned OK but no data will be returned
+ NO_CONTENT = httplib.NO_CONTENT
+
+ #: Bad Request
+ BAD_REQUEST = httplib.BAD_REQUEST
+
+ #: User is not authorized
+ UNAUTHORIZED = httplib.UNAUTHORIZED
+
+ #: The request is not allowed
+ FORBIDDEN = httplib.FORBIDDEN
+
+ #: The requested resource was not found
+ NOT_FOUND = httplib.NOT_FOUND
+
+ #: The request is not allowed
+ NOT_ALLOWED = httplib.METHOD_NOT_ALLOWED
+
+ #: Request timed out
+ TIMEOUT = httplib.REQUEST_TIMEOUT
+
+ #: Invalid request
+ CONFLICT = httplib.CONFLICT
+
+ #: Service Unavailable
+ BUSY = httplib.SERVICE_UNAVAILABLE
+
+
+class RestResult(object):
+ """Result from a REST API operation"""
+ def __init__(self, response=None, err=None):
+ """Initialize a RestResult containing the results from a REST call
+ :param response: HTTP response
+ """
+ self.response = response
+ self.error = err
+ self.data = ""
+ self.status = 0
+ if self.response is not None:
+ self.status = self.response.getcode()
+ result = self.response.read()
+ while result:
+ self.data += result
+ result = self.response.read()
+
+ if self.error is not None:
+ self.status = self.error.code
+ self.data = httplib.responses[self.status]
+
+ LOG.debug('response code: %s' % self.status)
+ LOG.debug('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
+ """
+ Exception.__init__(self, message)
+ self.code = status
+ self.name = name
+ self.msg = message
+ if status in httplib.responses:
+ self.msg = httplib.responses[status]
+
+ def __str__(self):
+ return "%d %s %s" % (self.code, self.name, self.msg)
+
+
+class RestClientURL(object):
+ """ZFSSA urllib2 client"""
+ def __init__(self, url, **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.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 == httplib.CREATED:
+ self.headers['x-auth-session'] = \
+ result.get_header('x-auth-session')
+ self.do_logout = True
+ LOG.info('ZFSSA version: %s' %
+ result.get_header('x-zfssa-version'))
+
+ elif result.status == httplib.NOT_FOUND:
+ raise RestClientError(result.status, name="ERR_RESTError",
+ message="REST Not Available: \
+ Please Upgrade")
+
+ except RestClientError as err:
+ del self.headers['authorization']
+ raise err
+
+ def login(self, auth_str):
+ """
+ Login to an appliance using a user name and password and 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 = 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()
+
+ def request(self, path, request, body=None, **kwargs):
+ """Make an HTTP request and return the results
+
+ :param path: Path used with the initiazed 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 is not None:
+ if isinstance(body, dict):
+ body = str(json.dumps(body))
+
+ if body and len(body):
+ out_hdrs['content-length'] = len(body)
+
+ zfssaurl = self._path(path, kwargs.get("base_path"))
+ req = urllib2.Request(zfssaurl, body, out_hdrs)
+ req.get_method = lambda: request
+ maxreqretries = kwargs.get("maxreqretries", 10)
+ retry = 0
+ response = None
+
+ LOG.debug('request: %s %s' % (request, zfssaurl))
+ LOG.debug('out headers: %s' % out_hdrs)
+ if body is not None and body != '':
+ LOG.debug('body: %s' % body)
+
+ while retry < maxreqretries:
+ try:
+ response = urllib2.urlopen(req, timeout=self.timeout)
+ except urllib2.HTTPError as err:
+ LOG.error('REST Not Available: %s' % err.code)
+ if err.code == httplib.SERVICE_UNAVAILABLE and \
+ retry < maxreqretries:
+ retry += 1
+ time.sleep(1)
+ LOG.error('Server Busy retry request: %s' % retry)
+ continue
+ if (err.code == httplib.UNAUTHORIZED or
+ err.code == httplib.INTERNAL_SERVER_ERROR) and \
+ '/access/v1' not in zfssaurl:
+ try:
+ LOG.error('Authorizing request retry: %s, %s' %
+ (zfssaurl, retry))
+ self.authorize()
+ req.add_header('x-auth-session',
+ self.headers['x-auth-session'])
+ except RestClientError:
+ pass
+ retry += 1
+ time.sleep(1)
+ continue
+
+ return RestResult(err=err)
+
+ except urllib2.URLError as err:
+ LOG.error('URLError: %s' % err.reason)
+ raise RestClientError(-1, name="ERR_URLError",
+ message=err.reason)
+
+ break
+
+ if response and response.getcode() == httplib.SERVICE_UNAVAILABLE and \
+ retry >= maxreqretries:
+ raise RestClientError(response.getcode(), name="ERR_HTTPError",
+ message="REST Not Available: Disabled")
+
+ return RestResult(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)