components/openstack/cinder/files/zfssa/restclient.py
author Drew Fisher <drew.fisher@oracle.com>
Wed, 11 Jun 2014 17:13:12 -0700
changeset 1944 56ac2df1785b
permissions -rw-r--r--
PSARC/2014/207 OpenStack Glance Update to Havana PSARC/2014/208 OpenStack Cinder Update to Havana PSARC/2014/209 OpenStack Keystone Update to Havana PSARC/2014/210 OpenStack Nova Update to Havana 18416146 Neutron agents (L3 and DHCP) should cleanup resources when they are disabled 18562372 Failed to create a new project under Horizon 18645763 ZFSSA Cinder Driver support 18686327 evs agent silently ignores user-specified pool allocation ranges 18702697 fibre channel volumes should be supported in the cinder volume driver 18734289 nova won't terminate failed kz deployments 18738371 cinder-volume:setup should account for commented-out zfs_volume_base 18738374 cinder-volume:setup should check for existence of configuration file 18826190 nova-compute fails due to nova.utils.to_bytes 18855698 Update OpenStack to Havana 2013.2.3 18855710 Update python-cinderclient to 1.0.9 18855743 Update python-keystoneclient to 0.8.0 18855754 Update python-neutronclient to 2.3.4 18855764 Update python-novaclient to 2.17.0 18855793 Update python-swiftclient to 2.1.0 18856992 External networks can be deleted even when floating IP addresses are in use 18857784 bake in some more openstack configuration 18884923 Incorrect locale facets in python modules for openstack 18913890 the error in _get_view_and_lun may cause the failure of deleting volumes 18943044 Disable 'Security Groups' tab in Horizon dashboard

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