added new reporting functions

Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/972a4f03
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/972a4f03
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/972a4f03

Branch: refs/heads/trunk
Commit: 972a4f032f58e2eba6064362157f66c8f600ba11
Parents: 7bc8798
Author: anthony-shaw <anthonys...@apache.org>
Authored: Mon Jun 6 22:31:12 2016 +1000
Committer: anthony-shaw <anthonys...@apache.org>
Committed: Mon Jun 6 22:31:12 2016 +1000

----------------------------------------------------------------------
 libcloud/common/base.py                         | 2392 +++++-----
 libcloud/common/dimensiondata.py                | 2997 ++++++------
 libcloud/compute/drivers/dimensiondata.py       |   90 +
 .../dimensiondata/detailed_usage_report.csv     |   42 +
 .../dimensiondata/summary_usage_report.csv      |   13 +
 libcloud/test/compute/test_dimensiondata.py     | 4324 +++++++++---------
 6 files changed, 5020 insertions(+), 4838 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/972a4f03/libcloud/common/base.py
----------------------------------------------------------------------
diff --git a/libcloud/common/base.py b/libcloud/common/base.py
index 318d28e..77d5152 100644
--- a/libcloud/common/base.py
+++ b/libcloud/common/base.py
@@ -1,1196 +1,1196 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You 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.
-
-import os
-import sys
-import ssl
-import socket
-import copy
-import binascii
-import time
-
-import xml.dom.minidom
-
-try:
-    from lxml import etree as ET
-except ImportError:
-    from xml.etree import ElementTree as ET
-
-from pipes import quote as pquote
-
-try:
-    import simplejson as json
-except:
-    import json
-
-import libcloud
-
-from libcloud.utils.py3 import PY3, PY25
-from libcloud.utils.py3 import httplib
-from libcloud.utils.py3 import urlparse
-from libcloud.utils.py3 import urlencode
-from libcloud.utils.py3 import StringIO
-from libcloud.utils.py3 import u
-from libcloud.utils.py3 import b
-
-from libcloud.utils.misc import lowercase_keys, retry
-from libcloud.utils.compression import decompress_data
-
-from libcloud.common.exceptions import exception_from_message
-from libcloud.common.types import LibcloudError, MalformedResponseError
-from libcloud.httplib_ssl import LibcloudHTTPConnection
-from libcloud.httplib_ssl import LibcloudHTTPSConnection
-
-__all__ = [
-    'RETRY_FAILED_HTTP_REQUESTS',
-
-    'BaseDriver',
-
-    'Connection',
-    'PollingConnection',
-    'ConnectionKey',
-    'ConnectionUserAndKey',
-    'CertificateConnection',
-    'LoggingHTTPConnection',
-    'LoggingHTTPSConnection',
-
-    'Response',
-    'HTTPResponse',
-    'JsonResponse',
-    'XmlResponse',
-    'RawResponse'
-]
-
-# Module level variable indicates if the failed HTTP requests should be retried
-RETRY_FAILED_HTTP_REQUESTS = False
-
-
-class LazyObject(object):
-    """An object that doesn't get initialized until accessed."""
-
-    @classmethod
-    def _proxy(cls, *lazy_init_args, **lazy_init_kwargs):
-        class Proxy(cls, object):
-            _lazy_obj = None
-
-            def __init__(self):
-                # Must override the lazy_cls __init__
-                pass
-
-            def __getattribute__(self, attr):
-                lazy_obj = object.__getattribute__(self, '_get_lazy_obj')()
-                return getattr(lazy_obj, attr)
-
-            def __setattr__(self, attr, value):
-                lazy_obj = object.__getattribute__(self, '_get_lazy_obj')()
-                setattr(lazy_obj, attr, value)
-
-            def _get_lazy_obj(self):
-                lazy_obj = object.__getattribute__(self, '_lazy_obj')
-                if lazy_obj is None:
-                    lazy_obj = cls(*lazy_init_args, **lazy_init_kwargs)
-                    object.__setattr__(self, '_lazy_obj', lazy_obj)
-                return lazy_obj
-
-        return Proxy()
-
-    @classmethod
-    def lazy(cls, *lazy_init_args, **lazy_init_kwargs):
-        """Create a lazily instantiated instance of the subclass, cls."""
-        return cls._proxy(*lazy_init_args, **lazy_init_kwargs)
-
-
-class HTTPResponse(httplib.HTTPResponse):
-    # On python 2.6 some calls can hang because HEAD isn't quite properly
-    # supported.
-    # In particular this happens on S3 when calls are made to get_object to
-    # objects that don't exist.
-    # This applies the behaviour from 2.7, fixing the hangs.
-    def read(self, amt=None):
-        if self.fp is None:
-            return ''
-
-        if self._method == 'HEAD':
-            self.close()
-            return ''
-
-        return httplib.HTTPResponse.read(self, amt)
-
-
-class Response(object):
-    """
-    A base Response class to derive from.
-    """
-
-    status = httplib.OK  # Response status code
-    headers = {}  # Response headers
-    body = None  # Raw response body
-    object = None  # Parsed response body
-
-    error = None  # Reason returned by the server.
-    connection = None  # Parent connection class
-    parse_zero_length_body = False
-
-    def __init__(self, response, connection):
-        """
-        :param response: HTTP response object. (optional)
-        :type response: :class:`httplib.HTTPResponse`
-
-        :param connection: Parent connection object.
-        :type connection: :class:`.Connection`
-        """
-        self.connection = connection
-
-        # http.client In Python 3 doesn't automatically lowercase the header
-        # names
-        self.headers = lowercase_keys(dict(response.getheaders()))
-        self.error = response.reason
-        self.status = response.status
-
-        # This attribute is set when using LoggingConnection.
-        original_data = getattr(response, '_original_data', None)
-
-        if original_data:
-            # LoggingConnection already decompresses data so it can log it
-            # which means we don't need to decompress it here.
-            self.body = response._original_data
-        else:
-            self.body = self._decompress_response(body=response.read(),
-                                                  headers=self.headers)
-
-        if PY3:
-            self.body = b(self.body).decode('utf-8')
-
-        if not self.success():
-            raise exception_from_message(code=self.status,
-                                         message=self.parse_error(),
-                                         headers=self.headers)
-
-        self.object = self.parse_body()
-
-    def parse_body(self):
-        """
-        Parse response body.
-
-        Override in a provider's subclass.
-
-        :return: Parsed body.
-        :rtype: ``str``
-        """
-        return self.body
-
-    def parse_error(self):
-        """
-        Parse the error messages.
-
-        Override in a provider's subclass.
-
-        :return: Parsed error.
-        :rtype: ``str``
-        """
-        return self.body
-
-    def success(self):
-        """
-        Determine if our request was successful.
-
-        The meaning of this can be arbitrary; did we receive OK status? Did
-        the node get created? Were we authenticated?
-
-        :rtype: ``bool``
-        :return: ``True`` or ``False``
-        """
-        return self.status in [httplib.OK, httplib.CREATED]
-
-    def _decompress_response(self, body, headers):
-        """
-        Decompress a response body if it is using deflate or gzip encoding.
-
-        :param body: Response body.
-        :type body: ``str``
-
-        :param headers: Response headers.
-        :type headers: ``dict``
-
-        :return: Decompressed response
-        :rtype: ``str``
-        """
-        encoding = headers.get('content-encoding', None)
-
-        if encoding in ['zlib', 'deflate']:
-            body = decompress_data('zlib', body)
-        elif encoding in ['gzip', 'x-gzip']:
-            body = decompress_data('gzip', body)
-        else:
-            body = body.strip()
-
-        return body
-
-
-class JsonResponse(Response):
-    """
-    A Base JSON Response class to derive from.
-    """
-
-    def parse_body(self):
-        if len(self.body) == 0 and not self.parse_zero_length_body:
-            return self.body
-
-        try:
-            body = json.loads(self.body)
-        except:
-            raise MalformedResponseError(
-                'Failed to parse JSON',
-                body=self.body,
-                driver=self.connection.driver)
-        return body
-
-    parse_error = parse_body
-
-
-class XmlResponse(Response):
-    """
-    A Base XML Response class to derive from.
-    """
-
-    def parse_body(self):
-        if len(self.body) == 0 and not self.parse_zero_length_body:
-            return self.body
-
-        try:
-            try:
-                body = ET.XML(self.body)
-            except ValueError:
-                # lxml wants a bytes and tests are basically hard-coded to str
-                body = ET.XML(self.body.encode('utf-8'))
-        except:
-            raise MalformedResponseError('Failed to parse XML',
-                                         body=self.body,
-                                         driver=self.connection.driver)
-        return body
-
-    parse_error = parse_body
-
-
-class RawResponse(Response):
-
-    def __init__(self, connection):
-        """
-        :param connection: Parent connection object.
-        :type connection: :class:`.Connection`
-        """
-        self._status = None
-        self._response = None
-        self._headers = {}
-        self._error = None
-        self._reason = None
-        self.connection = connection
-
-    @property
-    def response(self):
-        if not self._response:
-            response = self.connection.connection.getresponse()
-            self._response, self.body = response, response
-            if not self.success():
-                self.parse_error()
-        return self._response
-
-    @property
-    def status(self):
-        if not self._status:
-            self._status = self.response.status
-        return self._status
-
-    @property
-    def headers(self):
-        if not self._headers:
-            self._headers = lowercase_keys(dict(self.response.getheaders()))
-        return self._headers
-
-    @property
-    def reason(self):
-        if not self._reason:
-            self._reason = self.response.reason
-        return self._reason
-
-
-# TODO: Move this to a better location/package
-class LoggingConnection():
-    """
-    Debug class to log all HTTP(s) requests as they could be made
-    with the curl command.
-
-    :cvar log: file-like object that logs entries are written to.
-    """
-
-    log = None
-    http_proxy_used = False
-
-    def _log_response(self, r):
-        rv = "# -------- begin %d:%d response ----------\n" % (id(self), id(r))
-        ht = ""
-        v = r.version
-        if r.version == 10:
-            v = "HTTP/1.0"
-        if r.version == 11:
-            v = "HTTP/1.1"
-        ht += "%s %s %s\r\n" % (v, r.status, r.reason)
-        body = r.read()
-        for h in r.getheaders():
-            ht += "%s: %s\r\n" % (h[0].title(), h[1])
-        ht += "\r\n"
-
-        # this is evil. laugh with me. ha arharhrhahahaha
-        class fakesock(object):
-            def __init__(self, s):
-                self.s = s
-
-            def makefile(self, *args, **kwargs):
-                if PY3:
-                    from io import BytesIO
-                    cls = BytesIO
-                else:
-                    cls = StringIO
-
-                return cls(b(self.s))
-        rr = r
-        headers = lowercase_keys(dict(r.getheaders()))
-
-        encoding = headers.get('content-encoding', None)
-        content_type = headers.get('content-type', None)
-
-        if encoding in ['zlib', 'deflate']:
-            body = decompress_data('zlib', body)
-        elif encoding in ['gzip', 'x-gzip']:
-            body = decompress_data('gzip', body)
-
-        pretty_print = os.environ.get('LIBCLOUD_DEBUG_PRETTY_PRINT_RESPONSE',
-                                      False)
-
-        if r.chunked:
-            ht += "%x\r\n" % (len(body))
-            ht += body.decode('utf-8')
-            ht += "\r\n0\r\n"
-        else:
-            if pretty_print and content_type == 'application/json':
-                try:
-                    body = json.loads(body.decode('utf-8'))
-                    body = json.dumps(body, sort_keys=True, indent=4)
-                except:
-                    # Invalid JSON or server is lying about content-type
-                    pass
-            elif pretty_print and content_type == 'text/xml':
-                try:
-                    elem = xml.dom.minidom.parseString(body.decode('utf-8'))
-                    body = elem.toprettyxml()
-                except Exception:
-                    # Invalid XML
-                    pass
-
-            ht += u(body)
-
-        if sys.version_info >= (2, 6) and sys.version_info < (2, 7):
-            cls = HTTPResponse
-        else:
-            cls = httplib.HTTPResponse
-
-        rr = cls(sock=fakesock(ht), method=r._method,
-                 debuglevel=r.debuglevel)
-        rr.begin()
-        rv += ht
-        rv += ("\n# -------- end %d:%d response ----------\n"
-               % (id(self), id(r)))
-
-        rr._original_data = body
-        return (rr, rv)
-
-    def _log_curl(self, method, url, body, headers):
-        # pylint: disable=no-member
-
-        cmd = ["curl"]
-
-        if self.http_proxy_used:
-            if self.proxy_username and self.proxy_password:
-                proxy_url = 'http://%s:%s@%s:%s' % (self.proxy_username,
-                                                    self.proxy_password,
-                                                    self.proxy_host,
-                                                    self.proxy_port)
-            else:
-                proxy_url = 'http://%s:%s' % (self.proxy_host,
-                                              self.proxy_port)
-            proxy_url = pquote(proxy_url)
-            cmd.extend(['--proxy', proxy_url])
-
-        cmd.extend(['-i'])
-
-        if method.lower() == 'head':
-            # HEAD method need special handling
-            cmd.extend(["--head"])
-        else:
-            cmd.extend(["-X", pquote(method)])
-
-        for h in headers:
-            cmd.extend(["-H", pquote("%s: %s" % (h, headers[h]))])
-
-        cert_file = getattr(self, 'cert_file', None)
-
-        if cert_file:
-            cmd.extend(["--cert", pquote(cert_file)])
-
-        # TODO: in python 2.6, body can be a file-like object.
-        if body is not None and len(body) > 0:
-            cmd.extend(["--data-binary", pquote(body)])
-
-        cmd.extend(["--compress"])
-        cmd.extend([pquote("%s://%s:%d%s" % (self.protocol, self.host,
-                                             self.port, url))])
-        return " ".join(cmd)
-
-
-class LoggingHTTPSConnection(LoggingConnection, LibcloudHTTPSConnection):
-    """
-    Utility Class for logging HTTPS connections
-    """
-
-    protocol = 'https'
-
-    def getresponse(self):
-        r = LibcloudHTTPSConnection.getresponse(self)
-        if self.log is not None:
-            r, rv = self._log_response(r)
-            self.log.write(rv + "\n")
-            self.log.flush()
-        return r
-
-    def request(self, method, url, body=None, headers=None):
-        headers.update({'X-LC-Request-ID': str(id(self))})
-        if self.log is not None:
-            pre = "# -------- begin %d request ----------\n" % id(self)
-            self.log.write(pre +
-                           self._log_curl(method, url, body, headers) + "\n")
-            self.log.flush()
-        return LibcloudHTTPSConnection.request(self, method, url, body,
-                                               headers)
-
-
-class LoggingHTTPConnection(LoggingConnection, LibcloudHTTPConnection):
-    """
-    Utility Class for logging HTTP connections
-    """
-
-    protocol = 'http'
-
-    def getresponse(self):
-        r = LibcloudHTTPConnection.getresponse(self)
-        if self.log is not None:
-            r, rv = self._log_response(r)
-            self.log.write(rv + "\n")
-            self.log.flush()
-        return r
-
-    def request(self, method, url, body=None, headers=None):
-        headers.update({'X-LC-Request-ID': str(id(self))})
-        if self.log is not None:
-            pre = '# -------- begin %d request ----------\n' % id(self)
-            self.log.write(pre +
-                           self._log_curl(method, url, body, headers) + "\n")
-            self.log.flush()
-        return LibcloudHTTPConnection.request(self, method, url,
-                                              body, headers)
-
-
-class Connection(object):
-    """
-    A Base Connection class to derive from.
-    """
-    # conn_classes = (LoggingHTTPSConnection)
-    conn_classes = (LibcloudHTTPConnection, LibcloudHTTPSConnection)
-
-    responseCls = Response
-    rawResponseCls = RawResponse
-    connection = None
-    host = '127.0.0.1'
-    port = 443
-    timeout = None
-    secure = 1
-    driver = None
-    action = None
-    cache_busting = False
-    backoff = None
-    retry_delay = None
-
-    allow_insecure = True
-
-    def __init__(self, secure=True, host=None, port=None, url=None,
-                 timeout=None, proxy_url=None, retry_delay=None, backoff=None):
-        self.secure = secure and 1 or 0
-        self.ua = []
-        self.context = {}
-
-        if not self.allow_insecure and not secure:
-            # TODO: We should eventually switch to whitelist instead of
-            # blacklist approach
-            raise ValueError('Non https connections are not allowed (use '
-                             'secure=True)')
-
-        self.request_path = ''
-
-        if host:
-            self.host = host
-
-        if port is not None:
-            self.port = port
-        else:
-            if self.secure == 1:
-                self.port = 443
-            else:
-                self.port = 80
-
-        if url:
-            (self.host, self.port, self.secure,
-             self.request_path) = self._tuple_from_url(url)
-
-        self.timeout = timeout or self.timeout
-        self.retry_delay = retry_delay
-        self.backoff = backoff
-        self.proxy_url = proxy_url
-
-    def set_http_proxy(self, proxy_url):
-        """
-        Set a HTTP proxy which will be used with this connection.
-
-        :param proxy_url: Proxy URL (e.g. http://<hostname>:<port> without
-                          authentication and
-                          http://<username>:<password>@<hostname>:<port> for
-                          basic auth authentication information.
-        :type proxy_url: ``str``
-        """
-        self.proxy_url = proxy_url
-
-    def set_context(self, context):
-        if not isinstance(context, dict):
-            raise TypeError('context needs to be a dictionary')
-
-        self.context = context
-
-    def reset_context(self):
-        self.context = {}
-
-    def _tuple_from_url(self, url):
-        secure = 1
-        port = None
-        (scheme, netloc, request_path, param,
-         query, fragment) = urlparse.urlparse(url)
-
-        if scheme not in ['http', 'https']:
-            raise LibcloudError('Invalid scheme: %s in url %s' % (scheme, url))
-
-        if scheme == "http":
-            secure = 0
-
-        if ":" in netloc:
-            netloc, port = netloc.rsplit(":")
-            port = int(port)
-
-        if not port:
-            if scheme == "http":
-                port = 80
-            else:
-                port = 443
-
-        host = netloc
-        port = int(port)
-
-        return (host, port, secure, request_path)
-
-    def connect(self, host=None, port=None, base_url=None, **kwargs):
-        """
-        Establish a connection with the API server.
-
-        :type host: ``str``
-        :param host: Optional host to override our default
-
-        :type port: ``int``
-        :param port: Optional port to override our default
-
-        :returns: A connection
-        """
-        # prefer the attribute base_url if its set or sent
-        connection = None
-        secure = self.secure
-
-        # pylint: disable=no-member
-
-        if getattr(self, 'base_url', None) and base_url is None:
-            (host, port,
-             secure, request_path) = self._tuple_from_url(self.base_url)
-        elif base_url is not None:
-            (host, port,
-             secure, request_path) = self._tuple_from_url(base_url)
-        else:
-            host = host or self.host
-            port = port or self.port
-
-        # Make sure port is an int
-        port = int(port)
-
-        if not hasattr(kwargs, 'host'):
-            kwargs.update({'host': host})
-
-        if not hasattr(kwargs, 'port'):
-            kwargs.update({'port': port})
-
-        if not hasattr(kwargs, 'key_file') and hasattr(self, 'key_file'):
-            kwargs.update({'key_file': self.key_file})
-
-        if not hasattr(kwargs, 'cert_file') and hasattr(self, 'cert_file'):
-            kwargs.update({'cert_file': self.cert_file})
-
-        #  kwargs = {'host': host, 'port': int(port)}
-
-        # Timeout is only supported in Python 2.6 and later
-        # http://docs.python.org/library/httplib.html#httplib.HTTPConnection
-        if self.timeout and not PY25:
-            kwargs.update({'timeout': self.timeout})
-
-        if self.proxy_url:
-            kwargs.update({'proxy_url': self.proxy_url})
-
-        connection = self.conn_classes[secure](**kwargs)
-        # You can uncoment this line, if you setup a reverse proxy server
-        # which proxies to your endpoint, and lets you easily capture
-        # connections in cleartext when you setup the proxy to do SSL
-        # for you
-        # connection = self.conn_classes[False]("127.0.0.1", 8080)
-
-        self.connection = connection
-
-    def _user_agent(self):
-        user_agent_suffix = ' '.join(['(%s)' % x for x in self.ua])
-
-        if self.driver:
-            user_agent = 'libcloud/%s (%s) %s' % (
-                libcloud.__version__,
-                self.driver.name, user_agent_suffix)
-        else:
-            user_agent = 'libcloud/%s %s' % (
-                libcloud.__version__, user_agent_suffix)
-
-        return user_agent
-
-    def user_agent_append(self, token):
-        """
-        Append a token to a user agent string.
-
-        Users of the library should call this to uniquely identify their
-        requests to a provider.
-
-        :type token: ``str``
-        :param token: Token to add to the user agent.
-        """
-        self.ua.append(token)
-
-    def request(self, action, params=None, data=None, headers=None,
-                method='GET', raw=False):
-        """
-        Request a given `action`.
-
-        Basically a wrapper around the connection
-        object's `request` that does some helpful pre-processing.
-
-        :type action: ``str``
-        :param action: A path. This can include arguments. If included,
-            any extra parameters are appended to the existing ones.
-
-        :type params: ``dict``
-        :param params: Optional mapping of additional parameters to send. If
-            None, leave as an empty ``dict``.
-
-        :type data: ``unicode``
-        :param data: A body of data to send with the request.
-
-        :type headers: ``dict``
-        :param headers: Extra headers to add to the request
-            None, leave as an empty ``dict``.
-
-        :type method: ``str``
-        :param method: An HTTP method such as "GET" or "POST".
-
-        :type raw: ``bool``
-        :param raw: True to perform a "raw" request aka only send the headers
-                     and use the rawResponseCls class. This is used with
-                     storage API when uploading a file.
-
-        :return: An :class:`Response` instance.
-        :rtype: :class:`Response` instance
-
-        """
-        if params is None:
-            params = {}
-        else:
-            params = copy.copy(params)
-
-        if headers is None:
-            headers = {}
-        else:
-            headers = copy.copy(headers)
-
-        retry_enabled = os.environ.get('LIBCLOUD_RETRY_FAILED_HTTP_REQUESTS',
-                                       False) or RETRY_FAILED_HTTP_REQUESTS
-
-        action = self.morph_action_hook(action)
-        self.action = action
-        self.method = method
-        self.data = data
-
-        # Extend default parameters
-        params = self.add_default_params(params)
-
-        # Add cache busting parameters (if enabled)
-        if self.cache_busting and method == 'GET':
-            params = self._add_cache_busting_to_params(params=params)
-
-        # Extend default headers
-        headers = self.add_default_headers(headers)
-
-        # We always send a user-agent header
-        headers.update({'User-Agent': self._user_agent()})
-
-        # Indicate that we support gzip and deflate compression
-        headers.update({'Accept-Encoding': 'gzip,deflate'})
-
-        port = int(self.port)
-
-        if port not in (80, 443):
-            headers.update({'Host': "%s:%d" % (self.host, port)})
-        else:
-            headers.update({'Host': self.host})
-
-        if data:
-            data = self.encode_data(data)
-            headers['Content-Length'] = str(len(data))
-        elif method.upper() in ['POST', 'PUT'] and not raw:
-            # Only send Content-Length 0 with POST and PUT request.
-            #
-            # Note: Content-Length is not added when using "raw" mode means
-            # means that headers are upfront and the body is sent at some point
-            # later on. With raw mode user can specify Content-Length with
-            # "data" not being set.
-            headers['Content-Length'] = '0'
-
-        params, headers = self.pre_connect_hook(params, headers)
-
-        if params:
-            if '?' in action:
-                url = '&'.join((action, urlencode(params, doseq=True)))
-            else:
-                url = '?'.join((action, urlencode(params, doseq=True)))
-        else:
-            url = action
-
-        # Removed terrible hack...this a less-bad hack that doesn't execute a
-        # request twice, but it's still a hack.
-        self.connect()
-        try:
-            # @TODO: Should we just pass File object as body to request method
-            # instead of dealing with splitting and sending the file ourselves?
-            if raw:
-                self.connection.putrequest(method, url,
-                                           skip_host=1,
-                                           skip_accept_encoding=1)
-
-                for key, value in list(headers.items()):
-                    self.connection.putheader(key, str(value))
-
-                self.connection.endheaders()
-            else:
-                if retry_enabled:
-                    retry_request = retry(timeout=self.timeout,
-                                          retry_delay=self.retry_delay,
-                                          backoff=self.backoff)
-                    retry_request(self.connection.request)(method=method,
-                                                           url=url,
-                                                           body=data,
-                                                           headers=headers)
-                else:
-                    self.connection.request(method=method, url=url, body=data,
-                                            headers=headers)
-        except socket.gaierror:
-            e = sys.exc_info()[1]
-            message = str(e)
-            errno = getattr(e, 'errno', None)
-
-            if errno == -5:
-                # Throw a more-friendly exception on "no address associated
-                # with hostname" error. This error could simpli indicate that
-                # "host" Connection class attribute is set to an incorrect
-                # value
-                class_name = self.__class__.__name__
-                msg = ('%s. Perhaps "host" Connection class attribute '
-                       '(%s.connection) is set to an invalid, non-hostname '
-                       'value (%s)?' %
-                       (message, class_name, self.host))
-                raise socket.gaierror(msg)
-            self.reset_context()
-            raise e
-        except ssl.SSLError:
-            e = sys.exc_info()[1]
-            self.reset_context()
-            raise ssl.SSLError(str(e))
-
-        if raw:
-            responseCls = self.rawResponseCls
-            kwargs = {'connection': self}
-        else:
-            responseCls = self.responseCls
-            kwargs = {'connection': self,
-                      'response': self.connection.getresponse()}
-
-        try:
-            response = responseCls(**kwargs)
-        finally:
-            # Always reset the context after the request has completed
-            self.reset_context()
-
-        return response
-
-    def morph_action_hook(self, action):
-        return self.request_path + action
-
-    def add_default_params(self, params):
-        """
-        Adds default parameters (such as API key, version, etc.)
-        to the passed `params`
-
-        Should return a dictionary.
-        """
-        return params
-
-    def add_default_headers(self, headers):
-        """
-        Adds default headers (such as Authorization, X-Foo-Bar)
-        to the passed `headers`
-
-        Should return a dictionary.
-        """
-        return headers
-
-    def pre_connect_hook(self, params, headers):
-        """
-        A hook which is called before connecting to the remote server.
-        This hook can perform a final manipulation on the params, headers and
-        url parameters.
-
-        :type params: ``dict``
-        :param params: Request parameters.
-
-        :type headers: ``dict``
-        :param headers: Request headers.
-        """
-        return params, headers
-
-    def encode_data(self, data):
-        """
-        Encode body data.
-
-        Override in a provider's subclass.
-        """
-        return data
-
-    def _add_cache_busting_to_params(self, params):
-        """
-        Add cache busting parameter to the query parameters of a GET request.
-
-        Parameters are only added if "cache_busting" class attribute is set to
-        True.
-
-        Note: This should only be used with *naughty* providers which use
-        excessive caching of responses.
-        """
-        cache_busting_value = binascii.hexlify(os.urandom(8)).decode('ascii')
-
-        if isinstance(params, dict):
-            params['cache-busting'] = cache_busting_value
-        else:
-            params.append(('cache-busting', cache_busting_value))
-
-        return params
-
-
-class PollingConnection(Connection):
-    """
-    Connection class which can also work with the async APIs.
-
-    After initial requests, this class periodically polls for jobs status and
-    waits until the job has finished.
-    If job doesn't finish in timeout seconds, an Exception thrown.
-    """
-    poll_interval = 0.5
-    timeout = 200
-    request_method = 'request'
-
-    def async_request(self, action, params=None, data=None, headers=None,
-                      method='GET', context=None):
-        """
-        Perform an 'async' request to the specified path. Keep in mind that
-        this function is *blocking* and 'async' in this case means that the
-        hit URL only returns a job ID which is the periodically polled until
-        the job has completed.
-
-        This function works like this:
-
-        - Perform a request to the specified path. Response should contain a
-          'job_id'.
-
-        - Returned 'job_id' is then used to construct a URL which is used for
-          retrieving job status. Constructed URL is then periodically polled
-          until the response indicates that the job has completed or the
-          timeout of 'self.timeout' seconds has been reached.
-
-        :type action: ``str``
-        :param action: A path
-
-        :type params: ``dict``
-        :param params: Optional mapping of additional parameters to send. If
-            None, leave as an empty ``dict``.
-
-        :type data: ``unicode``
-        :param data: A body of data to send with the request.
-
-        :type headers: ``dict``
-        :param headers: Extra headers to add to the request
-            None, leave as an empty ``dict``.
-
-        :type method: ``str``
-        :param method: An HTTP method such as "GET" or "POST".
-
-        :type context: ``dict``
-        :param context: Context dictionary which is passed to the functions
-                        which construct initial and poll URL.
-
-        :return: An :class:`Response` instance.
-        :rtype: :class:`Response` instance
-        """
-
-        request = getattr(self, self.request_method)
-        kwargs = self.get_request_kwargs(action=action, params=params,
-                                         data=data, headers=headers,
-                                         method=method,
-                                         context=context)
-        response = request(**kwargs)
-        kwargs = self.get_poll_request_kwargs(response=response,
-                                              context=context,
-                                              request_kwargs=kwargs)
-
-        end = time.time() + self.timeout
-        completed = False
-        while time.time() < end and not completed:
-            response = request(**kwargs)
-            completed = self.has_completed(response=response)
-            if not completed:
-                time.sleep(self.poll_interval)
-
-        if not completed:
-            raise LibcloudError('Job did not complete in %s seconds' %
-                                (self.timeout))
-
-        return response
-
-    def get_request_kwargs(self, action, params=None, data=None, headers=None,
-                           method='GET', context=None):
-        """
-        Arguments which are passed to the initial request() call inside
-        async_request.
-        """
-        kwargs = {'action': action, 'params': params, 'data': data,
-                  'headers': headers, 'method': method}
-        return kwargs
-
-    def get_poll_request_kwargs(self, response, context, request_kwargs):
-        """
-        Return keyword arguments which are passed to the request() method when
-        polling for the job status.
-
-        :param response: Response object returned by poll request.
-        :type response: :class:`HTTPResponse`
-
-        :param request_kwargs: Kwargs previously used to initiate the
-                                  poll request.
-        :type response: ``dict``
-
-        :return ``dict`` Keyword arguments
-        """
-        raise NotImplementedError('get_poll_request_kwargs not implemented')
-
-    def has_completed(self, response):
-        """
-        Return job completion status.
-
-        :param response: Response object returned by poll request.
-        :type response: :class:`HTTPResponse`
-
-        :return ``bool`` True if the job has completed, False otherwise.
-        """
-        raise NotImplementedError('has_completed not implemented')
-
-
-class ConnectionKey(Connection):
-    """
-    Base connection class which accepts a single ``key`` argument.
-    """
-    def __init__(self, key, secure=True, host=None, port=None, url=None,
-                 timeout=None, proxy_url=None, backoff=None, retry_delay=None):
-        """
-        Initialize `user_id` and `key`; set `secure` to an ``int`` based on
-        passed value.
-        """
-        super(ConnectionKey, self).__init__(secure=secure, host=host,
-                                            port=port, url=url,
-                                            timeout=timeout,
-                                            proxy_url=proxy_url,
-                                            backoff=backoff,
-                                            retry_delay=retry_delay)
-        self.key = key
-
-
-class CertificateConnection(Connection):
-    """
-    Base connection class which accepts a single ``cert_file`` argument.
-    """
-    def __init__(self, cert_file, secure=True, host=None, port=None, url=None,
-                 proxy_url=None, timeout=None, backoff=None, retry_delay=None):
-        """
-        Initialize `cert_file`; set `secure` to an ``int`` based on
-        passed value.
-        """
-        super(CertificateConnection, self).__init__(secure=secure, host=host,
-                                                    port=port, url=url,
-                                                    timeout=timeout,
-                                                    backoff=backoff,
-                                                    retry_delay=retry_delay,
-                                                    proxy_url=proxy_url)
-
-        self.cert_file = cert_file
-
-
-class ConnectionUserAndKey(ConnectionKey):
-    """
-    Base connection class which accepts a ``user_id`` and ``key`` argument.
-    """
-
-    user_id = None
-
-    def __init__(self, user_id, key, secure=True, host=None, port=None,
-                 url=None, timeout=None, proxy_url=None,
-                 backoff=None, retry_delay=None):
-        super(ConnectionUserAndKey, self).__init__(key, secure=secure,
-                                                   host=host, port=port,
-                                                   url=url, timeout=timeout,
-                                                   backoff=backoff,
-                                                   retry_delay=retry_delay,
-                                                   proxy_url=proxy_url)
-        self.user_id = user_id
-
-
-class BaseDriver(object):
-    """
-    Base driver class from which other classes can inherit from.
-    """
-
-    connectionCls = ConnectionKey
-
-    def __init__(self, key, secret=None, secure=True, host=None, port=None,
-                 api_version=None, region=None, **kwargs):
-        """
-        :param    key:    API key or username to be used (required)
-        :type     key:    ``str``
-
-        :param    secret: Secret password to be used (required)
-        :type     secret: ``str``
-
-        :param    secure: Whether to use HTTPS or HTTP. Note: Some providers
-                            only support HTTPS, and it is on by default.
-        :type     secure: ``bool``
-
-        :param    host: Override hostname used for connections.
-        :type     host: ``str``
-
-        :param    port: Override port used for connections.
-        :type     port: ``int``
-
-        :param    api_version: Optional API version. Only used by drivers
-                                 which support multiple API versions.
-        :type     api_version: ``str``
-
-        :param region: Optional driver region. Only used by drivers which
-                       support multiple regions.
-        :type region: ``str``
-
-        :rtype: ``None``
-        """
-
-        self.key = key
-        self.secret = secret
-        self.secure = secure
-        args = [self.key]
-
-        if self.secret is not None:
-            args.append(self.secret)
-
-        args.append(secure)
-
-        if host is not None:
-            args.append(host)
-
-        if port is not None:
-            args.append(port)
-
-        self.api_version = api_version
-        self.region = region
-
-        conn_kwargs = self._ex_connection_class_kwargs()
-
-        # Note: We do that to make sure those additional arguments which are
-        # provided via "_ex_connection_class_kwargs" are not overriden with
-        # None
-        additional_kwargs = ['timeout', 'retry_delay', 'backoff', 'proxy_url']
-        for kwarg_name in additional_kwargs:
-            value = kwargs.pop(kwarg_name, None)
-
-            # Constructor argument has precedence over
-            # _ex_connection_class_kwargs kwarg
-            if value is not None or kwarg_name not in conn_kwargs:
-                conn_kwargs[kwarg_name] = value
-
-        self.connection = self.connectionCls(*args, **conn_kwargs)
-
-        self.connection.driver = self
-        self.connection.connect()
-
-    @classmethod
-    def list_regions(cls):
-        """
-        Method which returns a list of the available / supported regions.
-
-        :rtype: ``list`` of ``str``
-        """
-        return []
-
-    def _ex_connection_class_kwargs(self):
-        """
-        Return extra connection keyword arguments which are passed to the
-        Connection class constructor.
-        """
-        return {}
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+import os
+import sys
+import ssl
+import socket
+import copy
+import binascii
+import time
+
+import xml.dom.minidom
+
+try:
+    from lxml import etree as ET
+except ImportError:
+    from xml.etree import ElementTree as ET
+
+from pipes import quote as pquote
+
+try:
+    import simplejson as json
+except:
+    import json
+
+import libcloud
+
+from libcloud.utils.py3 import PY3, PY25
+from libcloud.utils.py3 import httplib
+from libcloud.utils.py3 import urlparse
+from libcloud.utils.py3 import urlencode
+from libcloud.utils.py3 import StringIO
+from libcloud.utils.py3 import u
+from libcloud.utils.py3 import b
+
+from libcloud.utils.misc import lowercase_keys, retry
+from libcloud.utils.compression import decompress_data
+
+from libcloud.common.exceptions import exception_from_message
+from libcloud.common.types import LibcloudError, MalformedResponseError
+from libcloud.httplib_ssl import LibcloudHTTPConnection
+from libcloud.httplib_ssl import LibcloudHTTPSConnection
+
+__all__ = [
+    'RETRY_FAILED_HTTP_REQUESTS',
+
+    'BaseDriver',
+
+    'Connection',
+    'PollingConnection',
+    'ConnectionKey',
+    'ConnectionUserAndKey',
+    'CertificateConnection',
+    'LoggingHTTPConnection',
+    'LoggingHTTPSConnection',
+
+    'Response',
+    'HTTPResponse',
+    'JsonResponse',
+    'XmlResponse',
+    'RawResponse'
+]
+
+# Module level variable indicates if the failed HTTP requests should be retried
+RETRY_FAILED_HTTP_REQUESTS = False
+
+
+class LazyObject(object):
+    """An object that doesn't get initialized until accessed."""
+
+    @classmethod
+    def _proxy(cls, *lazy_init_args, **lazy_init_kwargs):
+        class Proxy(cls, object):
+            _lazy_obj = None
+
+            def __init__(self):
+                # Must override the lazy_cls __init__
+                pass
+
+            def __getattribute__(self, attr):
+                lazy_obj = object.__getattribute__(self, '_get_lazy_obj')()
+                return getattr(lazy_obj, attr)
+
+            def __setattr__(self, attr, value):
+                lazy_obj = object.__getattribute__(self, '_get_lazy_obj')()
+                setattr(lazy_obj, attr, value)
+
+            def _get_lazy_obj(self):
+                lazy_obj = object.__getattribute__(self, '_lazy_obj')
+                if lazy_obj is None:
+                    lazy_obj = cls(*lazy_init_args, **lazy_init_kwargs)
+                    object.__setattr__(self, '_lazy_obj', lazy_obj)
+                return lazy_obj
+
+        return Proxy()
+
+    @classmethod
+    def lazy(cls, *lazy_init_args, **lazy_init_kwargs):
+        """Create a lazily instantiated instance of the subclass, cls."""
+        return cls._proxy(*lazy_init_args, **lazy_init_kwargs)
+
+
+class HTTPResponse(httplib.HTTPResponse):
+    # On python 2.6 some calls can hang because HEAD isn't quite properly
+    # supported.
+    # In particular this happens on S3 when calls are made to get_object to
+    # objects that don't exist.
+    # This applies the behaviour from 2.7, fixing the hangs.
+    def read(self, amt=None):
+        if self.fp is None:
+            return ''
+
+        if self._method == 'HEAD':
+            self.close()
+            return ''
+
+        return httplib.HTTPResponse.read(self, amt)
+
+
+class Response(object):
+    """
+    A base Response class to derive from.
+    """
+
+    status = httplib.OK  # Response status code
+    headers = {}  # Response headers
+    body = None  # Raw response body
+    object = None  # Parsed response body
+
+    error = None  # Reason returned by the server.
+    connection = None  # Parent connection class
+    parse_zero_length_body = False
+
+    def __init__(self, response, connection):
+        """
+        :param response: HTTP response object. (optional)
+        :type response: :class:`httplib.HTTPResponse`
+
+        :param connection: Parent connection object.
+        :type connection: :class:`.Connection`
+        """
+        self.connection = connection
+
+        # http.client In Python 3 doesn't automatically lowercase the header
+        # names
+        self.headers = lowercase_keys(dict(response.getheaders()))
+        self.error = response.reason
+        self.status = response.status
+
+        # This attribute is set when using LoggingConnection.
+        original_data = getattr(response, '_original_data', None)
+
+        if original_data:
+            # LoggingConnection already decompresses data so it can log it
+            # which means we don't need to decompress it here.
+            self.body = response._original_data
+        else:
+            self.body = self._decompress_response(body=response.read(),
+                                                  headers=self.headers)
+
+        if PY3:
+            self.body = b(self.body).decode('utf-8')
+
+        if not self.success():
+            raise exception_from_message(code=self.status,
+                                         message=self.parse_error(),
+                                         headers=self.headers)
+
+        self.object = self.parse_body()
+
+    def parse_body(self):
+        """
+        Parse response body.
+
+        Override in a provider's subclass.
+
+        :return: Parsed body.
+        :rtype: ``str``
+        """
+        return self.body
+
+    def parse_error(self):
+        """
+        Parse the error messages.
+
+        Override in a provider's subclass.
+
+        :return: Parsed error.
+        :rtype: ``str``
+        """
+        return self.body
+
+    def success(self):
+        """
+        Determine if our request was successful.
+
+        The meaning of this can be arbitrary; did we receive OK status? Did
+        the node get created? Were we authenticated?
+
+        :rtype: ``bool``
+        :return: ``True`` or ``False``
+        """
+        return self.status in [httplib.OK, httplib.CREATED]
+
+    def _decompress_response(self, body, headers):
+        """
+        Decompress a response body if it is using deflate or gzip encoding.
+
+        :param body: Response body.
+        :type body: ``str``
+
+        :param headers: Response headers.
+        :type headers: ``dict``
+
+        :return: Decompressed response
+        :rtype: ``str``
+        """
+        encoding = headers.get('content-encoding', None)
+
+        if encoding in ['zlib', 'deflate']:
+            body = decompress_data('zlib', body)
+        elif encoding in ['gzip', 'x-gzip']:
+            body = decompress_data('gzip', body)
+        else:
+            body = body.strip()
+
+        return body
+
+
+class JsonResponse(Response):
+    """
+    A Base JSON Response class to derive from.
+    """
+
+    def parse_body(self):
+        if len(self.body) == 0 and not self.parse_zero_length_body:
+            return self.body
+
+        try:
+            body = json.loads(self.body)
+        except:
+            raise MalformedResponseError(
+                'Failed to parse JSON',
+                body=self.body,
+                driver=self.connection.driver)
+        return body
+
+    parse_error = parse_body
+
+
+class XmlResponse(Response):
+    """
+    A Base XML Response class to derive from.
+    """
+
+    def parse_body(self):
+        if len(self.body) == 0 and not self.parse_zero_length_body:
+            return self.body
+
+        try:
+            try:
+                body = ET.XML(self.body)
+            except ValueError:
+                # lxml wants a bytes and tests are basically hard-coded to str
+                body = ET.XML(self.body.encode('utf-8'))
+        except:
+            raise MalformedResponseError('Failed to parse XML',
+                                         body=self.body,
+                                         driver=self.connection.driver)
+        return body
+
+    parse_error = parse_body
+
+
+class RawResponse(Response):
+
+    def __init__(self, connection):
+        """
+        :param connection: Parent connection object.
+        :type connection: :class:`.Connection`
+        """
+        self._status = None
+        self._response = None
+        self._headers = {}
+        self._error = None
+        self._reason = None
+        self.connection = connection
+
+    @property
+    def response(self):
+        if not self._response:
+            response = self.connection.connection.getresponse()
+            self._response, self.body = response, response
+            if not self.success():
+                self.parse_error()
+        return self._response
+
+    @property
+    def status(self):
+        if not self._status:
+            self._status = self.response.status
+        return self._status
+
+    @property
+    def headers(self):
+        if not self._headers:
+            self._headers = lowercase_keys(dict(self.response.getheaders()))
+        return self._headers
+
+    @property
+    def reason(self):
+        if not self._reason:
+            self._reason = self.response.reason
+        return self._reason
+
+
+# TODO: Move this to a better location/package
+class LoggingConnection():
+    """
+    Debug class to log all HTTP(s) requests as they could be made
+    with the curl command.
+
+    :cvar log: file-like object that logs entries are written to.
+    """
+
+    log = None
+    http_proxy_used = False
+
+    def _log_response(self, r):
+        rv = "# -------- begin %d:%d response ----------\n" % (id(self), id(r))
+        ht = ""
+        v = r.version
+        if r.version == 10:
+            v = "HTTP/1.0"
+        if r.version == 11:
+            v = "HTTP/1.1"
+        ht += "%s %s %s\r\n" % (v, r.status, r.reason)
+        body = r.read()
+        for h in r.getheaders():
+            ht += "%s: %s\r\n" % (h[0].title(), h[1])
+        ht += "\r\n"
+
+        # this is evil. laugh with me. ha arharhrhahahaha
+        class fakesock(object):
+            def __init__(self, s):
+                self.s = s
+
+            def makefile(self, *args, **kwargs):
+                if PY3:
+                    from io import BytesIO
+                    cls = BytesIO
+                else:
+                    cls = StringIO
+
+                return cls(b(self.s))
+        rr = r
+        headers = lowercase_keys(dict(r.getheaders()))
+
+        encoding = headers.get('content-encoding', None)
+        content_type = headers.get('content-type', None)
+
+        if encoding in ['zlib', 'deflate']:
+            body = decompress_data('zlib', body)
+        elif encoding in ['gzip', 'x-gzip']:
+            body = decompress_data('gzip', body)
+
+        pretty_print = os.environ.get('LIBCLOUD_DEBUG_PRETTY_PRINT_RESPONSE',
+                                      False)
+
+        if r.chunked:
+            ht += "%x\r\n" % (len(body))
+            ht += body.decode('utf-8')
+            ht += "\r\n0\r\n"
+        else:
+            if pretty_print and content_type == 'application/json':
+                try:
+                    body = json.loads(body.decode('utf-8'))
+                    body = json.dumps(body, sort_keys=True, indent=4)
+                except:
+                    # Invalid JSON or server is lying about content-type
+                    pass
+            elif pretty_print and content_type == 'text/xml':
+                try:
+                    elem = xml.dom.minidom.parseString(body.decode('utf-8'))
+                    body = elem.toprettyxml()
+                except Exception:
+                    # Invalid XML
+                    pass
+
+            ht += u(body)
+
+        if sys.version_info >= (2, 6) and sys.version_info < (2, 7):
+            cls = HTTPResponse
+        else:
+            cls = httplib.HTTPResponse
+
+        rr = cls(sock=fakesock(ht), method=r._method,
+                 debuglevel=r.debuglevel)
+        rr.begin()
+        rv += ht
+        rv += ("\n# -------- end %d:%d response ----------\n"
+               % (id(self), id(r)))
+
+        rr._original_data = body
+        return (rr, rv)
+
+    def _log_curl(self, method, url, body, headers):
+        # pylint: disable=no-member
+
+        cmd = ["curl"]
+
+        if self.http_proxy_used:
+            if self.proxy_username and self.proxy_password:
+                proxy_url = 'http://%s:%s@%s:%s' % (self.proxy_username,
+                                                    self.proxy_password,
+                                                    self.proxy_host,
+                                                    self.proxy_port)
+            else:
+                proxy_url = 'http://%s:%s' % (self.proxy_host,
+                                              self.proxy_port)
+            proxy_url = pquote(proxy_url)
+            cmd.extend(['--proxy', proxy_url])
+
+        cmd.extend(['-i'])
+
+        if method.lower() == 'head':
+            # HEAD method need special handling
+            cmd.extend(["--head"])
+        else:
+            cmd.extend(["-X", pquote(method)])
+
+        for h in headers:
+            cmd.extend(["-H", pquote("%s: %s" % (h, headers[h]))])
+
+        cert_file = getattr(self, 'cert_file', None)
+
+        if cert_file:
+            cmd.extend(["--cert", pquote(cert_file)])
+
+        # TODO: in python 2.6, body can be a file-like object.
+        if body is not None and len(body) > 0:
+            cmd.extend(["--data-binary", pquote(body)])
+
+        cmd.extend(["--compress"])
+        cmd.extend([pquote("%s://%s:%d%s" % (self.protocol, self.host,
+                                             self.port, url))])
+        return " ".join(cmd)
+
+
+class LoggingHTTPSConnection(LoggingConnection, LibcloudHTTPSConnection):
+    """
+    Utility Class for logging HTTPS connections
+    """
+
+    protocol = 'https'
+
+    def getresponse(self):
+        r = LibcloudHTTPSConnection.getresponse(self)
+        if self.log is not None:
+            r, rv = self._log_response(r)
+            self.log.write(rv + "\n")
+            self.log.flush()
+        return r
+
+    def request(self, method, url, body=None, headers=None):
+        headers.update({'X-LC-Request-ID': str(id(self))})
+        if self.log is not None:
+            pre = "# -------- begin %d request ----------\n" % id(self)
+            self.log.write(pre +
+                           self._log_curl(method, url, body, headers) + "\n")
+            self.log.flush()
+        return LibcloudHTTPSConnection.request(self, method, url, body,
+                                               headers)
+
+
+class LoggingHTTPConnection(LoggingConnection, LibcloudHTTPConnection):
+    """
+    Utility Class for logging HTTP connections
+    """
+
+    protocol = 'http'
+
+    def getresponse(self):
+        r = LibcloudHTTPConnection.getresponse(self)
+        if self.log is not None:
+            r, rv = self._log_response(r)
+            self.log.write(rv + "\n")
+            self.log.flush()
+        return r
+
+    def request(self, method, url, body=None, headers=None):
+        headers.update({'X-LC-Request-ID': str(id(self))})
+        if self.log is not None:
+            pre = '# -------- begin %d request ----------\n' % id(self)
+            self.log.write(pre +
+                           self._log_curl(method, url, body, headers) + "\n")
+            self.log.flush()
+        return LibcloudHTTPConnection.request(self, method, url,
+                                              body, headers)
+
+
+class Connection(object):
+    """
+    A Base Connection class to derive from.
+    """
+    # conn_classes = (LoggingHTTPSConnection)
+    conn_classes = (LibcloudHTTPConnection, LibcloudHTTPSConnection)
+
+    responseCls = Response
+    rawResponseCls = RawResponse
+    connection = None
+    host = '127.0.0.1'
+    port = 443
+    timeout = None
+    secure = 1
+    driver = None
+    action = None
+    cache_busting = False
+    backoff = None
+    retry_delay = None
+
+    allow_insecure = True
+
+    def __init__(self, secure=True, host=None, port=None, url=None,
+                 timeout=None, proxy_url=None, retry_delay=None, backoff=None):
+        self.secure = secure and 1 or 0
+        self.ua = []
+        self.context = {}
+
+        if not self.allow_insecure and not secure:
+            # TODO: We should eventually switch to whitelist instead of
+            # blacklist approach
+            raise ValueError('Non https connections are not allowed (use '
+                             'secure=True)')
+
+        self.request_path = ''
+
+        if host:
+            self.host = host
+
+        if port is not None:
+            self.port = port
+        else:
+            if self.secure == 1:
+                self.port = 443
+            else:
+                self.port = 80
+
+        if url:
+            (self.host, self.port, self.secure,
+             self.request_path) = self._tuple_from_url(url)
+
+        self.timeout = timeout or self.timeout
+        self.retry_delay = retry_delay
+        self.backoff = backoff
+        self.proxy_url = proxy_url
+
+    def set_http_proxy(self, proxy_url):
+        """
+        Set a HTTP proxy which will be used with this connection.
+
+        :param proxy_url: Proxy URL (e.g. http://<hostname>:<port> without
+                          authentication and
+                          http://<username>:<password>@<hostname>:<port> for
+                          basic auth authentication information.
+        :type proxy_url: ``str``
+        """
+        self.proxy_url = proxy_url
+
+    def set_context(self, context):
+        if not isinstance(context, dict):
+            raise TypeError('context needs to be a dictionary')
+
+        self.context = context
+
+    def reset_context(self):
+        self.context = {}
+
+    def _tuple_from_url(self, url):
+        secure = 1
+        port = None
+        (scheme, netloc, request_path, param,
+         query, fragment) = urlparse.urlparse(url)
+
+        if scheme not in ['http', 'https']:
+            raise LibcloudError('Invalid scheme: %s in url %s' % (scheme, url))
+
+        if scheme == "http":
+            secure = 0
+
+        if ":" in netloc:
+            netloc, port = netloc.rsplit(":")
+            port = int(port)
+
+        if not port:
+            if scheme == "http":
+                port = 80
+            else:
+                port = 443
+
+        host = netloc
+        port = int(port)
+
+        return (host, port, secure, request_path)
+
+    def connect(self, host=None, port=None, base_url=None, **kwargs):
+        """
+        Establish a connection with the API server.
+
+        :type host: ``str``
+        :param host: Optional host to override our default
+
+        :type port: ``int``
+        :param port: Optional port to override our default
+
+        :returns: A connection
+        """
+        # prefer the attribute base_url if its set or sent
+        connection = None
+        secure = self.secure
+
+        # pylint: disable=no-member
+
+        if getattr(self, 'base_url', None) and base_url is None:
+            (host, port,
+             secure, request_path) = self._tuple_from_url(self.base_url)
+        elif base_url is not None:
+            (host, port,
+             secure, request_path) = self._tuple_from_url(base_url)
+        else:
+            host = host or self.host
+            port = port or self.port
+
+        # Make sure port is an int
+        port = int(port)
+
+        if not hasattr(kwargs, 'host'):
+            kwargs.update({'host': host})
+
+        if not hasattr(kwargs, 'port'):
+            kwargs.update({'port': port})
+
+        if not hasattr(kwargs, 'key_file') and hasattr(self, 'key_file'):
+            kwargs.update({'key_file': self.key_file})
+
+        if not hasattr(kwargs, 'cert_file') and hasattr(self, 'cert_file'):
+            kwargs.update({'cert_file': self.cert_file})
+
+        #  kwargs = {'host': host, 'port': int(port)}
+
+        # Timeout is only supported in Python 2.6 and later
+        # http://docs.python.org/library/httplib.html#httplib.HTTPConnection
+        if self.timeout and not PY25:
+            kwargs.update({'timeout': self.timeout})
+
+        if self.proxy_url:
+            kwargs.update({'proxy_url': self.proxy_url})
+
+        connection = self.conn_classes[secure](**kwargs)
+        # You can uncoment this line, if you setup a reverse proxy server
+        # which proxies to your endpoint, and lets you easily capture
+        # connections in cleartext when you setup the proxy to do SSL
+        # for you
+        # connection = self.conn_classes[False]("127.0.0.1", 8080)
+
+        self.connection = connection
+
+    def _user_agent(self):
+        user_agent_suffix = ' '.join(['(%s)' % x for x in self.ua])
+
+        if self.driver:
+            user_agent = 'libcloud/%s (%s) %s' % (
+                libcloud.__version__,
+                self.driver.name, user_agent_suffix)
+        else:
+            user_agent = 'libcloud/%s %s' % (
+                libcloud.__version__, user_agent_suffix)
+
+        return user_agent
+
+    def user_agent_append(self, token):
+        """
+        Append a token to a user agent string.
+
+        Users of the library should call this to uniquely identify their
+        requests to a provider.
+
+        :type token: ``str``
+        :param token: Token to add to the user agent.
+        """
+        self.ua.append(token)
+
+    def request(self, action, params=None, data=None, headers=None,
+                method='GET', raw=False):
+        """
+        Request a given `action`.
+
+        Basically a wrapper around the connection
+        object's `request` that does some helpful pre-processing.
+
+        :type action: ``str``
+        :param action: A path. This can include arguments. If included,
+            any extra parameters are appended to the existing ones.
+
+        :type params: ``dict``
+        :param params: Optional mapping of additional parameters to send. If
+            None, leave as an empty ``dict``.
+
+        :type data: ``unicode``
+        :param data: A body of data to send with the request.
+
+        :type headers: ``dict``
+        :param headers: Extra headers to add to the request
+            None, leave as an empty ``dict``.
+
+        :type method: ``str``
+        :param method: An HTTP method such as "GET" or "POST".
+
+        :type raw: ``bool``
+        :param raw: True to perform a "raw" request aka only send the headers
+                     and use the rawResponseCls class. This is used with
+                     storage API when uploading a file.
+
+        :return: An :class:`Response` instance.
+        :rtype: :class:`Response` instance
+
+        """
+        if params is None:
+            params = {}
+        else:
+            params = copy.copy(params)
+
+        if headers is None:
+            headers = {}
+        else:
+            headers = copy.copy(headers)
+
+        retry_enabled = os.environ.get('LIBCLOUD_RETRY_FAILED_HTTP_REQUESTS',
+                                       False) or RETRY_FAILED_HTTP_REQUESTS
+
+        action = self.morph_action_hook(action)
+        self.action = action
+        self.method = method
+        self.data = data
+
+        # Extend default parameters
+        params = self.add_default_params(params)
+
+        # Add cache busting parameters (if enabled)
+        if self.cache_busting and method == 'GET':
+            params = self._add_cache_busting_to_params(params=params)
+
+        # Extend default headers
+        headers = self.add_default_headers(headers)
+
+        # We always send a user-agent header
+        headers.update({'User-Agent': self._user_agent()})
+
+        # Indicate that we support gzip and deflate compression
+        headers.update({'Accept-Encoding': 'gzip,deflate'})
+
+        port = int(self.port)
+
+        if port not in (80, 443):
+            headers.update({'Host': "%s:%d" % (self.host, port)})
+        else:
+            headers.update({'Host': self.host})
+
+        if data:
+            data = self.encode_data(data)
+            headers['Content-Length'] = str(len(data))
+        elif method.upper() in ['POST', 'PUT'] and not raw:
+            # Only send Content-Length 0 with POST and PUT request.
+            #
+            # Note: Content-Length is not added when using "raw" mode means
+            # means that headers are upfront and the body is sent at some point
+            # later on. With raw mode user can specify Content-Length with
+            # "data" not being set.
+            headers['Content-Length'] = '0'
+
+        params, headers = self.pre_connect_hook(params, headers)
+
+        if params:
+            if '?' in action:
+                url = '&'.join((action, urlencode(params, doseq=True)))
+            else:
+                url = '?'.join((action, urlencode(params, doseq=True)))
+        else:
+            url = action
+
+        # Removed terrible hack...this a less-bad hack that doesn't execute a
+        # request twice, but it's still a hack.
+        self.connect()
+        try:
+            # @TODO: Should we just pass File object as body to request method
+            # instead of dealing with splitting and sending the file ourselves?
+            if raw and method.upper() in ['POST', 'PUT']:
+                self.connection.putrequest(method, url,
+                                           skip_host=1,
+                                           skip_accept_encoding=1)
+
+                for key, value in list(headers.items()):
+                    self.connection.putheader(key, str(value))
+
+                self.connection.endheaders()
+            else:
+                if retry_enabled:
+                    retry_request = retry(timeout=self.timeout,
+                                          retry_delay=self.retry_delay,
+                                          backoff=self.backoff)
+                    retry_request(self.connection.request)(method=method,
+                                                           url=url,
+                                                           body=data,
+                                                           headers=headers)
+                else:
+                    self.connection.request(method=method, url=url, body=data,
+                                            headers=headers)
+        except socket.gaierror:
+            e = sys.exc_info()[1]
+            message = str(e)
+            errno = getattr(e, 'errno', None)
+
+            if errno == -5:
+                # Throw a more-friendly exception on "no address associated
+                # with hostname" error. This error could simpli indicate that
+                # "host" Connection class attribute is set to an incorrect
+                # value
+                class_name = self.__class__.__name__
+                msg = ('%s. Perhaps "host" Connection class attribute '
+                       '(%s.connection) is set to an invalid, non-hostname '
+                       'value (%s)?' %
+                       (message, class_name, self.host))
+                raise socket.gaierror(msg)
+            self.reset_context()
+            raise e
+        except ssl.SSLError:
+            e = sys.exc_info()[1]
+            self.reset_context()
+            raise ssl.SSLError(str(e))
+
+        if raw:
+            responseCls = self.rawResponseCls
+            kwargs = {'connection': self}
+        else:
+            responseCls = self.responseCls
+            kwargs = {'connection': self,
+                      'response': self.connection.getresponse()}
+
+        try:
+            response = responseCls(**kwargs)
+        finally:
+            # Always reset the context after the request has completed
+            self.reset_context()
+
+        return response
+
+    def morph_action_hook(self, action):
+        return self.request_path + action
+
+    def add_default_params(self, params):
+        """
+        Adds default parameters (such as API key, version, etc.)
+        to the passed `params`
+
+        Should return a dictionary.
+        """
+        return params
+
+    def add_default_headers(self, headers):
+        """
+        Adds default headers (such as Authorization, X-Foo-Bar)
+        to the passed `headers`
+
+        Should return a dictionary.
+        """
+        return headers
+
+    def pre_connect_hook(self, params, headers):
+        """
+        A hook which is called before connecting to the remote server.
+        This hook can perform a final manipulation on the params, headers and
+        url parameters.
+
+        :type params: ``dict``
+        :param params: Request parameters.
+
+        :type headers: ``dict``
+        :param headers: Request headers.
+        """
+        return params, headers
+
+    def encode_data(self, data):
+        """
+        Encode body data.
+
+        Override in a provider's subclass.
+        """
+        return data
+
+    def _add_cache_busting_to_params(self, params):
+        """
+        Add cache busting parameter to the query parameters of a GET request.
+
+        Parameters are only added if "cache_busting" class attribute is set to
+        True.
+
+        Note: This should only be used with *naughty* providers which use
+        excessive caching of responses.
+        """
+        cache_busting_value = binascii.hexlify(os.urandom(8)).decode('ascii')
+
+        if isinstance(params, dict):
+            params['cache-busting'] = cache_busting_value
+        else:
+            params.append(('cache-busting', cache_busting_value))
+
+        return params
+
+
+class PollingConnection(Connection):
+    """
+    Connection class which can also work with the async APIs.
+
+    After initial requests, this class periodically polls for jobs status and
+    waits until the job has finished.
+    If job doesn't finish in timeout seconds, an Exception thrown.
+    """
+    poll_interval = 0.5
+    timeout = 200
+    request_method = 'request'
+
+    def async_request(self, action, params=None, data=None, headers=None,
+                      method='GET', context=None):
+        """
+        Perform an 'async' request to the specified path. Keep in mind that
+        this function is *blocking* and 'async' in this case means that the
+        hit URL only returns a job ID which is the periodically polled until
+        the job has completed.
+
+        This function works like this:
+
+        - Perform a request to the specified path. Response should contain a
+          'job_id'.
+
+        - Returned 'job_id' is then used to construct a URL which is used for
+          retrieving job status. Constructed URL is then periodically polled
+          until the response indicates that the job has completed or the
+          timeout of 'self.timeout' seconds has been reached.
+
+        :type action: ``str``
+        :param action: A path
+
+        :type params: ``dict``
+        :param params: Optional mapping of additional parameters to send. If
+            None, leave as an empty ``dict``.
+
+        :type data: ``unicode``
+        :param data: A body of data to send with the request.
+
+        :type headers: ``dict``
+        :param headers: Extra headers to add to the request
+            None, leave as an empty ``dict``.
+
+        :type method: ``str``
+        :param method: An HTTP method such as "GET" or "POST".
+
+        :type context: ``dict``
+        :param context: Context dictionary which is passed to the functions
+                        which construct initial and poll URL.
+
+        :return: An :class:`Response` instance.
+        :rtype: :class:`Response` instance
+        """
+
+        request = getattr(self, self.request_method)
+        kwargs = self.get_request_kwargs(action=action, params=params,
+                                         data=data, headers=headers,
+                                         method=method,
+                                         context=context)
+        response = request(**kwargs)
+        kwargs = self.get_poll_request_kwargs(response=response,
+                                              context=context,
+                                              request_kwargs=kwargs)
+
+        end = time.time() + self.timeout
+        completed = False
+        while time.time() < end and not completed:
+            response = request(**kwargs)
+            completed = self.has_completed(response=response)
+            if not completed:
+                time.sleep(self.poll_interval)
+
+        if not completed:
+            raise LibcloudError('Job did not complete in %s seconds' %
+                                (self.timeout))
+
+        return response
+
+    def get_request_kwargs(self, action, params=None, data=None, headers=None,
+                           method='GET', context=None):
+        """
+        Arguments which are passed to the initial request() call inside
+        async_request.
+        """
+        kwargs = {'action': action, 'params': params, 'data': data,
+                  'headers': headers, 'method': method}
+        return kwargs
+
+    def get_poll_request_kwargs(self, response, context, request_kwargs):
+        """
+        Return keyword arguments which are passed to the request() method when
+        polling for the job status.
+
+        :param response: Response object returned by poll request.
+        :type response: :class:`HTTPResponse`
+
+        :param request_kwargs: Kwargs previously used to initiate the
+                                  poll request.
+        :type response: ``dict``
+
+        :return ``dict`` Keyword arguments
+        """
+        raise NotImplementedError('get_poll_request_kwargs not implemented')
+
+    def has_completed(self, response):
+        """
+        Return job completion status.
+
+        :param response: Response object returned by poll request.
+        :type response: :class:`HTTPResponse`
+
+        :return ``bool`` True if the job has completed, False otherwise.
+        """
+        raise NotImplementedError('has_completed not implemented')
+
+
+class ConnectionKey(Connection):
+    """
+    Base connection class which accepts a single ``key`` argument.
+    """
+    def __init__(self, key, secure=True, host=None, port=None, url=None,
+                 timeout=None, proxy_url=None, backoff=None, retry_delay=None):
+        """
+        Initialize `user_id` and `key`; set `secure` to an ``int`` based on
+        passed value.
+        """
+        super(ConnectionKey, self).__init__(secure=secure, host=host,
+                                            port=port, url=url,
+                                            timeout=timeout,
+                                            proxy_url=proxy_url,
+                                            backoff=backoff,
+                                            retry_delay=retry_delay)
+        self.key = key
+
+
+class CertificateConnection(Connection):
+    """
+    Base connection class which accepts a single ``cert_file`` argument.
+    """
+    def __init__(self, cert_file, secure=True, host=None, port=None, url=None,
+                 proxy_url=None, timeout=None, backoff=None, retry_delay=None):
+        """
+        Initialize `cert_file`; set `secure` to an ``int`` based on
+        passed value.
+        """
+        super(CertificateConnection, self).__init__(secure=secure, host=host,
+                                                    port=port, url=url,
+                                                    timeout=timeout,
+                                                    backoff=backoff,
+                                                    retry_delay=retry_delay,
+                                                    proxy_url=proxy_url)
+
+        self.cert_file = cert_file
+
+
+class ConnectionUserAndKey(ConnectionKey):
+    """
+    Base connection class which accepts a ``user_id`` and ``key`` argument.
+    """
+
+    user_id = None
+
+    def __init__(self, user_id, key, secure=True, host=None, port=None,
+                 url=None, timeout=None, proxy_url=None,
+                 backoff=None, retry_delay=None):
+        super(ConnectionUserAndKey, self).__init__(key, secure=secure,
+                                                   host=host, port=port,
+                                                   url=url, timeout=timeout,
+                                                   backoff=backoff,
+                                                   retry_delay=retry_delay,
+                                                   proxy_url=proxy_url)
+        self.user_id = user_id
+
+
+class BaseDriver(object):
+    """
+    Base driver class from which other classes can inherit from.
+    """
+
+    connectionCls = ConnectionKey
+
+    def __init__(self, key, secret=None, secure=True, host=None, port=None,
+                 api_version=None, region=None, **kwargs):
+        """
+        :param    key:    API key or username to be used (required)
+        :type     key:    ``str``
+
+        :param    secret: Secret password to be used (required)
+        :type     secret: ``str``
+
+        :param    secure: Whether to use HTTPS or HTTP. Note: Some providers
+                            only support HTTPS, and it is on by default.
+        :type     secure: ``bool``
+
+        :param    host: Override hostname used for connections.
+        :type     host: ``str``
+
+        :param    port: Override port used for connections.
+        :type     port: ``int``
+
+        :param    api_version: Optional API version. Only used by drivers
+                                 which support multiple API versions.
+        :type     api_version: ``str``
+
+        :param region: Optional driver region. Only used by drivers which
+                       support multiple regions.
+        :type region: ``str``
+
+        :rtype: ``None``
+        """
+
+        self.key = key
+        self.secret = secret
+        self.secure = secure
+        args = [self.key]
+
+        if self.secret is not None:
+            args.append(self.secret)
+
+        args.append(secure)
+
+        if host is not None:
+            args.append(host)
+
+        if port is not None:
+            args.append(port)
+
+        self.api_version = api_version
+        self.region = region
+
+        conn_kwargs = self._ex_connection_class_kwargs()
+
+        # Note: We do that to make sure those additional arguments which are
+        # provided via "_ex_connection_class_kwargs" are not overriden with
+        # None
+        additional_kwargs = ['timeout', 'retry_delay', 'backoff', 'proxy_url']
+        for kwarg_name in additional_kwargs:
+            value = kwargs.pop(kwarg_name, None)
+
+            # Constructor argument has precedence over
+            # _ex_connection_class_kwargs kwarg
+            if value is not None or kwarg_name not in conn_kwargs:
+                conn_kwargs[kwarg_name] = value
+
+        self.connection = self.connectionCls(*args, **conn_kwargs)
+
+        self.connection.driver = self
+        self.connection.connect()
+
+    @classmethod
+    def list_regions(cls):
+        """
+        Method which returns a list of the available / supported regions.
+
+        :rtype: ``list`` of ``str``
+        """
+        return []
+
+    def _ex_connection_class_kwargs(self):
+        """
+        Return extra connection keyword arguments which are passed to the
+        Connection class constructor.
+        """
+        return {}

Reply via email to