Repository: apache/libcloud

Issue:
Better support for Dimension Data private cloud
Improved the support for Dimension Data private cloud and public cloud driver instantiation



Discussion:

Resolving patch:
diff --git a/libcloud/common/base.py b/libcloud/common/base.py
--- 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:
+                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 {}
diff --git a/libcloud/common/dimensiondata.py b/libcloud/common/dimensiondata.py
--- a/libcloud/common/dimensiondata.py
+++ b/libcloud/common/dimensiondata.py
@@ -1,1492 +1,1505 @@
-# 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.
-"""
-Dimension Data Common Components
-"""
-from base64 import b64encode
-from time import sleep
-from libcloud.utils.py3 import httplib
-from libcloud.utils.py3 import b
-from libcloud.common.base import ConnectionUserAndKey, XmlResponse
-from libcloud.common.types import LibcloudError, InvalidCredsError
-from libcloud.compute.base import Node
-from libcloud.utils.py3 import basestring
-from libcloud.utils.xml import findtext
-
-# Roadmap / TODO:
-#
-# 1.0 - Copied from OpSource API, named provider details.
-
-# setup a few variables to represent all of the DimensionData cloud namespaces
-NAMESPACE_BASE = "http://oec.api.opsource.net/schemas"
-ORGANIZATION_NS = NAMESPACE_BASE + "/organization"
-SERVER_NS = NAMESPACE_BASE + "/server"
-NETWORK_NS = NAMESPACE_BASE + "/network"
-DIRECTORY_NS = NAMESPACE_BASE + "/directory"
-GENERAL_NS = NAMESPACE_BASE + "/general"
-BACKUP_NS = NAMESPACE_BASE + "/backup"
-
-# API 2.0 Namespaces and URNs
-TYPES_URN = "urn:didata.com:api:cloud:types"
-
-# API end-points
-API_ENDPOINTS = {
-    'dd-na': {
-        'name': 'North America (NA)',
-        'host': 'api-na.dimensiondata.com',
-        'vendor': 'DimensionData'
-    },
-    'dd-eu': {
-        'name': 'Europe (EU)',
-        'host': 'api-eu.dimensiondata.com',
-        'vendor': 'DimensionData'
-    },
-    'dd-au': {
-        'name': 'Australia (AU)',
-        'host': 'api-au.dimensiondata.com',
-        'vendor': 'DimensionData'
-    },
-    'dd-au-gov': {
-        'name': 'Australia Canberra ACT (AU)',
-        'host': 'api-canberra.dimensiondata.com',
-        'vendor': 'DimensionData'
-    },
-    'dd-af': {
-        'name': 'Africa (AF)',
-        'host': 'api-mea.dimensiondata.com',
-        'vendor': 'DimensionData'
-    },
-    'dd-ap': {
-        'name': 'Asia Pacific (AP)',
-        'host': 'api-ap.dimensiondata.com',
-        'vendor': 'DimensionData'
-    },
-    'dd-latam': {
-        'name': 'South America (LATAM)',
-        'host': 'api-latam.dimensiondata.com',
-        'vendor': 'DimensionData'
-    },
-    'dd-canada': {
-        'name': 'Canada (CA)',
-        'host': 'api-canada.dimensiondata.com',
-        'vendor': 'DimensionData'
-    },
-    'is-na': {
-        'name': 'North America (NA)',
-        'host': 'usapi.cloud.is.co.za',
-        'vendor': 'InternetSolutions'
-    },
-    'is-eu': {
-        'name': 'Europe (EU)',
-        'host': 'euapi.cloud.is.co.za',
-        'vendor': 'InternetSolutions'
-    },
-    'is-au': {
-        'name': 'Australia (AU)',
-        'host': 'auapi.cloud.is.co.za',
-        'vendor': 'InternetSolutions'
-    },
-    'is-af': {
-        'name': 'Africa (AF)',
-        'host': 'meaapi.cloud.is.co.za',
-        'vendor': 'InternetSolutions'
-    },
-    'is-ap': {
-        'name': 'Asia Pacific (AP)',
-        'host': 'apapi.cloud.is.co.za',
-        'vendor': 'InternetSolutions'
-    },
-    'is-latam': {
-        'name': 'South America (LATAM)',
-        'host': 'latamapi.cloud.is.co.za',
-        'vendor': 'InternetSolutions'
-    },
-    'is-canada': {
-        'name': 'Canada (CA)',
-        'host': 'canadaapi.cloud.is.co.za',
-        'vendor': 'InternetSolutions'
-    },
-    'ntta-na': {
-        'name': 'North America (NA)',
-        'host': 'cloudapi.nttamerica.com',
-        'vendor': 'NTTNorthAmerica'
-    },
-    'ntta-eu': {
-        'name': 'Europe (EU)',
-        'host': 'eucloudapi.nttamerica.com',
-        'vendor': 'NTTNorthAmerica'
-    },
-    'ntta-au': {
-        'name': 'Australia (AU)',
-        'host': 'aucloudapi.nttamerica.com',
-        'vendor': 'NTTNorthAmerica'
-    },
-    'ntta-af': {
-        'name': 'Africa (AF)',
-        'host': 'sacloudapi.nttamerica.com',
-        'vendor': 'NTTNorthAmerica'
-    },
-    'ntta-ap': {
-        'name': 'Asia Pacific (AP)',
-        'host': 'hkcloudapi.nttamerica.com',
-        'vendor': 'NTTNorthAmerica'
-    },
-    'cisco-na': {
-        'name': 'North America (NA)',
-        'host': 'iaas-api-na.cisco-ccs.com',
-        'vendor': 'Cisco'
-    },
-    'cisco-eu': {
-        'name': 'Europe (EU)',
-        'host': 'iaas-api-eu.cisco-ccs.com',
-        'vendor': 'Cisco'
-    },
-    'cisco-au': {
-        'name': 'Australia (AU)',
-        'host': 'iaas-api-au.cisco-ccs.com',
-        'vendor': 'Cisco'
-    },
-    'cisco-af': {
-        'name': 'Africa (AF)',
-        'host': 'iaas-api-mea.cisco-ccs.com',
-        'vendor': 'Cisco'
-    },
-    'cisco-ap': {
-        'name': 'Asia Pacific (AP)',
-        'host': 'iaas-api-ap.cisco-ccs.com',
-        'vendor': 'Cisco'
-    },
-    'cisco-latam': {
-        'name': 'South America (LATAM)',
-        'host': 'iaas-api-sa.cisco-ccs.com',
-        'vendor': 'Cisco'
-    },
-    'cisco-canada': {
-        'name': 'Canada (CA)',
-        'host': 'iaas-api-ca.cisco-ccs.com',
-        'vendor': 'Cisco'
-    },
-    'med1-il': {
-        'name': 'Israel (IL)',
-        'host': 'api.cloud.med-1.com',
-        'vendor': 'Med-1'
-    },
-    'med1-na': {
-        'name': 'North America (NA)',
-        'host': 'api-na.cloud.med-1.com',
-        'vendor': 'Med-1'
-    },
-    'med1-eu': {
-        'name': 'Europe (EU)',
-        'host': 'api-eu.cloud.med-1.com',
-        'vendor': 'Med-1'
-    },
-    'med1-au': {
-        'name': 'Australia (AU)',
-        'host': 'api-au.cloud.med-1.com',
-        'vendor': 'Med-1'
-    },
-    'med1-af': {
-        'name': 'Africa (AF)',
-        'host': 'api-af.cloud.med-1.com',
-        'vendor': 'Med-1'
-    },
-    'med1-ap': {
-        'name': 'Asia Pacific (AP)',
-        'host': 'api-ap.cloud.med-1.com',
-        'vendor': 'Med-1'
-    },
-    'med1-latam': {
-        'name': 'South America (LATAM)',
-        'host': 'api-sa.cloud.med-1.com',
-        'vendor': 'Med-1'
-    },
-    'med1-canada': {
-        'name': 'Canada (CA)',
-        'host': 'api-ca.cloud.med-1.com',
-        'vendor': 'Med-1'
-    },
-    'indosat-id': {
-        'name': 'Indonesia (ID)',
-        'host': 'iaas-api.indosat.com',
-        'vendor': 'Indosat'
-    },
-    'indosat-na': {
-        'name': 'North America (NA)',
-        'host': 'iaas-usapi.indosat.com',
-        'vendor': 'Indosat'
-    },
-    'indosat-eu': {
-        'name': 'Europe (EU)',
-        'host': 'iaas-euapi.indosat.com',
-        'vendor': 'Indosat'
-    },
-    'indosat-au': {
-        'name': 'Australia (AU)',
-        'host': 'iaas-auapi.indosat.com',
-        'vendor': 'Indosat'
-    },
-    'indosat-af': {
-        'name': 'Africa (AF)',
-        'host': 'iaas-afapi.indosat.com',
-        'vendor': 'Indosat'
-    },
-    'bsnl-in': {
-        'name': 'India (IN)',
-        'host': 'api.bsnlcloud.com',
-        'vendor': 'BSNL'
-    },
-    'bsnl-na': {
-        'name': 'North America (NA)',
-        'host': 'usapi.bsnlcloud.com',
-        'vendor': 'BSNL'
-    },
-    'bsnl-eu': {
-        'name': 'Europe (EU)',
-        'host': 'euapi.bsnlcloud.com',
-        'vendor': 'BSNL'
-    },
-    'bsnl-au': {
-        'name': 'Australia (AU)',
-        'host': 'auapi.bsnlcloud.com',
-        'vendor': 'BSNL'
-    },
-    'bsnl-af': {
-        'name': 'Africa (AF)',
-        'host': 'afapi.bsnlcloud.com',
-        'vendor': 'BSNL'
-    },
-}
-
-# Default API end-point for the base connection class.
-DEFAULT_REGION = 'dd-na'
-
-BAD_CODE_XML_ELEMENTS = (
-    ('responseCode', SERVER_NS),
-    ('responseCode', TYPES_URN),
-    ('result', GENERAL_NS)
-)
-
-BAD_MESSAGE_XML_ELEMENTS = (
-    ('message', SERVER_NS),
-    ('message', TYPES_URN),
-    ('resultDetail', GENERAL_NS)
-)
-
-
-def dd_object_to_id(obj, obj_type, id_value='id'):
-    """
-    Takes in a DD object or string and prints out it's id
-    This is a helper method, as many of our functions can take either an object
-    or a string, and we need an easy way of converting them
-
-    :param obj: The object to get the id for
-    :type  obj: ``object``
-
-    :param  func: The function to call, e.g. ex_get_vlan. Note: This
-                  function needs to return an object which has ``status``
-                  attribute.
-    :type   func: ``function``
-
-    :rtype: ``str``
-    """
-    if isinstance(obj, obj_type):
-        return getattr(obj, id_value)
-    elif isinstance(obj, (basestring)):
-        return obj
-    else:
-        raise TypeError(
-            "Invalid type %s looking for basestring or %s"
-            % (type(obj).__name__, obj_type.__name__)
-        )
-
-
-class NetworkDomainServicePlan(object):
-    ESSENTIALS = "ESSENTIALS"
-    ADVANCED = "ADVANCED"
-
-
-class DimensionDataResponse(XmlResponse):
-    def parse_error(self):
-        if self.status == httplib.UNAUTHORIZED:
-            raise InvalidCredsError(self.body)
-        elif self.status == httplib.FORBIDDEN:
-            raise InvalidCredsError(self.body)
-
-        body = self.parse_body()
-
-        if self.status == httplib.BAD_REQUEST:
-            for response_code in BAD_CODE_XML_ELEMENTS:
-                code = findtext(body, response_code[0], response_code[1])
-                if code is not None:
-                    break
-            for message in BAD_MESSAGE_XML_ELEMENTS:
-                message = findtext(body, message[0], message[1])
-                if message is not None:
-                    break
-            raise DimensionDataAPIException(code=code,
-                                            msg=message,
-                                            driver=self.connection.driver)
-        if self.status is not httplib.OK:
-            raise DimensionDataAPIException(code=self.status,
-                                            msg=body,
-                                            driver=self.connection.driver)
-
-        return self.body
-
-
-class DimensionDataAPIException(LibcloudError):
-    def __init__(self, code, msg, driver):
-        self.code = code
-        self.msg = msg
-        self.driver = driver
-
-    def __str__(self):
-        return "%s: %s" % (self.code, self.msg)
-
-    def __repr__(self):
-        return ("<DimensionDataAPIException: code='%s', msg='%s'>" %
-                (self.code, self.msg))
-
-
-class DimensionDataConnection(ConnectionUserAndKey):
-    """
-    Connection class for the DimensionData driver
-    """
-
-    api_path_version_1 = '/oec'
-    api_path_version_2 = '/caas'
-    api_version_1 = '0.9'
-    api_version_2 = '2.2'
-
-    _orgId = None
-    responseCls = DimensionDataResponse
-
-    allow_insecure = False
-
-    def __init__(self, user_id, key, secure=True, host=None, port=None,
-                 url=None, timeout=None, proxy_url=None, **conn_kwargs):
-        super(DimensionDataConnection, self).__init__(
-            user_id=user_id,
-            key=key,
-            secure=secure,
-            host=host, port=port,
-            url=url, timeout=timeout,
-            proxy_url=proxy_url)
-
-        if conn_kwargs['region']:
-            self.host = conn_kwargs['region']['host']
-
-    def add_default_headers(self, headers):
-        headers['Authorization'] = \
-            ('Basic %s' % b64encode(b('%s:%s' % (self.user_id,
-                                                 self.key))).decode('utf-8'))
-        headers['Content-Type'] = 'application/xml'
-        return headers
-
-    def request_api_1(self, action, params=None, data='',
-                      headers=None, method='GET'):
-        action = "%s/%s/%s" % (self.api_path_version_1,
-                               self.api_version_1, action)
-
-        return super(DimensionDataConnection, self).request(
-            action=action,
-            params=params, data=data,
-            method=method, headers=headers)
-
-    def request_api_2(self, path, action, params=None, data='',
-                      headers=None, method='GET'):
-        action = "%s/%s/%s/%s" % (self.api_path_version_2,
-                                  self.api_version_2, path, action)
-
-        return super(DimensionDataConnection, self).request(
-            action=action,
-            params=params, data=data,
-            method=method, headers=headers)
-
-    def request_with_orgId_api_1(self, action, params=None, data='',
-                                 headers=None, method='GET'):
-        action = "%s/%s" % (self.get_resource_path_api_1(), action)
-
-        return super(DimensionDataConnection, self).request(
-            action=action,
-            params=params, data=data,
-            method=method, headers=headers)
-
-    def request_with_orgId_api_2(self, action, params=None, data='',
-                                 headers=None, method='GET'):
-        action = "%s/%s" % (self.get_resource_path_api_2(), action)
-
-        return super(DimensionDataConnection, self).request(
-            action=action,
-            params=params, data=data,
-            method=method, headers=headers)
-
-    def paginated_request_with_orgId_api_2(self, action, params=None, data='',
-                                           headers=None, method='GET',
-                                           page_size=250):
-        """
-        A paginated request to the MCP2.0 API
-        This essentially calls out to request_with_orgId_api_2 for each page
-        and yields the response to make a generator
-        This generator can be looped through to grab all the pages.
-
-        :param action: The resource to access (i.e. 'network/vlan')
-        :type  action: ``str``
-
-        :param params: Parameters to give to the action
-        :type  params: ``dict`` or ``None``
-
-        :param data: The data payload to be added to the request
-        :type  data: ``str``
-
-        :param headers: Additional header to be added to the request
-        :type  headers: ``str`` or ``dict`` or ``None``
-
-        :param method: HTTP Method for the request (i.e. 'GET', 'POST')
-        :type  method: ``str``
-
-        :param page_size: The size of each page to be returned
-                          Note: Max page size in MCP2.0 is currently 250
-        :type  page_size: ``int``
-        """
-        if params is None:
-            params = {}
-        params['pageSize'] = page_size
-
-        paged_resp = self.request_with_orgId_api_2(action, params,
-                                                   data, headers,
-                                                   method).object
-        yield paged_resp
-        paged_resp = paged_resp or {}
-
-        while int(paged_resp.get('pageCount')) >= \
-                int(paged_resp.get('pageSize')):
-            params['pageNumber'] = int(paged_resp.get('pageNumber')) + 1
-            paged_resp = self.request_with_orgId_api_2(action, params,
-                                                       data, headers,
-                                                       method).object
-            yield paged_resp
-
-    def get_resource_path_api_1(self):
-        """
-        This method returns a resource path which is necessary for referencing
-        resources that require a full path instead of just an ID, such as
-        networks, and customer snapshots.
-        """
-        return ("%s/%s/%s" % (self.api_path_version_1, self.api_version_1,
-                              self._get_orgId()))
-
-    def get_resource_path_api_2(self):
-        """
-        This method returns a resource path which is necessary for referencing
-        resources that require a full path instead of just an ID, such as
-        networks, and customer snapshots.
-        """
-        return ("%s/%s/%s" % (self.api_path_version_2, self.api_version_2,
-                              self._get_orgId()))
-
-    def wait_for_state(self, state, func, poll_interval=2, timeout=60, *args,
-                       **kwargs):
-        """
-        Wait for the function which returns a instance with field status/state
-        to match.
-
-        Keep polling func until one of the desired states is matched
-
-        :param state: Either the desired state (`str`) or a `list` of states
-        :type  state: ``str`` or ``list``
-
-        :param  func: The function to call, e.g. ex_get_vlan. Note: This
-                      function needs to return an object which has ``status``
-                      attribute.
-        :type   func: ``function``
-
-        :param  poll_interval: The number of seconds to wait between checks
-        :type   poll_interval: `int`
-
-        :param  timeout: The total number of seconds to wait to reach a state
-        :type   timeout: `int`
-
-        :param  args: The arguments for func
-        :type   args: Positional arguments
-
-        :param  kwargs: The arguments for func
-        :type   kwargs: Keyword arguments
-
-        :return: Result from the calling function.
-        """
-        cnt = 0
-        while cnt < timeout / poll_interval:
-            result = func(*args, **kwargs)
-            if isinstance(result, Node):
-                object_state = result.state
-            else:
-                object_state = result.status
-
-            if object_state is state or object_state in state:
-                return result
-            sleep(poll_interval)
-            cnt += 1
-
-        msg = 'Status check for object %s timed out' % (result)
-        raise DimensionDataAPIException(code=object_state,
-                                        msg=msg,
-                                        driver=self.driver)
-
-    def _get_orgId(self):
-        """
-        Send the /myaccount API request to DimensionData cloud and parse the
-        'orgId' from the XML response object. We need the orgId to use most
-        of the other API functions
-        """
-        if self._orgId is None:
-            body = self.request_api_1('myaccount').object
-            self._orgId = findtext(body, 'orgId', DIRECTORY_NS)
-        return self._orgId
-
-    def get_account_details(self):
-        """
-        Get the details of this account
-
-        :rtype: :class:`DimensionDataAccountDetails`
-        """
-        body = self.request_api_1('myaccount').object
-        return DimensionDataAccountDetails(
-            user_name=findtext(body, 'userName', DIRECTORY_NS),
-            full_name=findtext(body, 'fullName', DIRECTORY_NS),
-            first_name=findtext(body, 'firstName', DIRECTORY_NS),
-            last_name=findtext(body, 'lastName', DIRECTORY_NS),
-            email=findtext(body, 'emailAddress', DIRECTORY_NS))
-
-
-class DimensionDataAccountDetails(object):
-    """
-    Dimension Data account class details
-    """
-    def __init__(self, user_name, full_name, first_name, last_name, email):
-        self.user_name = user_name
-        self.full_name = full_name
-        self.first_name = first_name
-        self.last_name = last_name
-        self.email = email
-
-
-class DimensionDataStatus(object):
-    """
-    DimensionData API pending operation status class
-        action, request_time, user_name, number_of_steps, update_time,
-        step.name, step.number, step.percent_complete, failure_reason,
-    """
-    def __init__(self, action=None, request_time=None, user_name=None,
-                 number_of_steps=None, update_time=None, step_name=None,
-                 step_number=None, step_percent_complete=None,
-                 failure_reason=None):
-        self.action = action
-        self.request_time = request_time
-        self.user_name = user_name
-        self.number_of_steps = number_of_steps
-        self.update_time = update_time
-        self.step_name = step_name
-        self.step_number = step_number
-        self.step_percent_complete = step_percent_complete
-        self.failure_reason = failure_reason
-
-    def __repr__(self):
-        return (('<DimensionDataStatus: action=%s, request_time=%s, '
-                 'user_name=%s, number_of_steps=%s, update_time=%s, '
-                 'step_name=%s, step_number=%s, '
-                 'step_percent_complete=%s, failure_reason=%s>')
-                % (self.action, self.request_time, self.user_name,
-                   self.number_of_steps, self.update_time, self.step_name,
-                   self.step_number, self.step_percent_complete,
-                   self.failure_reason))
-
-
-class DimensionDataNetwork(object):
-    """
-    DimensionData network with location.
-    """
-
-    def __init__(self, id, name, description, location, private_net,
-                 multicast, status):
-        self.id = str(id)
-        self.name = name
-        self.description = description
-        self.location = location
-        self.private_net = private_net
-        self.multicast = multicast
-        self.status = status
-
-    def __repr__(self):
-        return (('<DimensionDataNetwork: id=%s, name=%s, description=%s, '
-                 'location=%s, private_net=%s, multicast=%s>')
-                % (self.id, self.name, self.description, self.location,
-                   self.private_net, self.multicast))
-
-
-class DimensionDataNetworkDomain(object):
-    """
-    DimensionData network domain with location.
-    """
-
-    def __init__(self, id, name, description, location, status, plan):
-        self.id = str(id)
-        self.name = name
-        self.description = description
-        self.location = location
-        self.status = status
-        self.plan = plan
-
-    def __repr__(self):
-        return (('<DimensionDataNetworkDomain: id=%s, name=%s, '
-                 'description=%s, location=%s, status=%s>')
-                % (self.id, self.name, self.description, self.location,
-                   self.status))
-
-
-class DimensionDataPublicIpBlock(object):
-    """
-    DimensionData Public IP Block with location.
-    """
-
-    def __init__(self, id, base_ip, size, location, network_domain,
-                 status):
-        self.id = str(id)
-        self.base_ip = base_ip
-        self.size = size
-        self.location = location
-        self.network_domain = network_domain
-        self.status = status
-
-    def __repr__(self):
-        return (('<DimensionDataNetworkDomain: id=%s, base_ip=%s, '
-                 'size=%s, location=%s, status=%s>')
-                % (self.id, self.base_ip, self.size, self.location,
-                   self.status))
-
-
-class DimensionDataServerCpuSpecification(object):
-    """
-    A class that represents the specification of the CPU(s) for a
-    node
-    """
-    def __init__(self, cpu_count, cores_per_socket, performance):
-        """
-        Instantiate a new :class:`DimensionDataServerCpuSpecification`
-
-        :param cpu_count: The number of CPUs
-        :type  cpu_count: ``int``
-
-        :param cores_per_socket: The number of cores per socket, the
-            recommendation is 1
-        :type  cores_per_socket: ``int``
-
-        :param performance: The performance type, e.g. HIGHPERFORMANCE
-        :type  performance: ``str``
-        """
-        self.cpu_count = cpu_count
-        self.cores_per_socket = cores_per_socket
-        self.performance = performance
-
-    def __repr__(self):
-        return (('<DimensionDataServerCpuSpecification: '
-                 'cpu_count=%s, cores_per_socket=%s, '
-                 'performance=%s>')
-                % (self.cpu_count, self.cores_per_socket, self.performance))
-
-
-class DimensionDataServerDisk(object):
-    """
-    A class that represents the disk on a server
-    """
-    def __init__(self, id, scsi_id, size_gb, speed, state):
-        """
-        Instantiate a new :class:`DimensionDataServerDisk`
-
-        :param id: The id of the disk
-        :type  id: ``str``
-
-        :param scsi_id: Representation for scsi
-        :type  scsi_id: ``int``
-
-        :param size_gb: Size of the disk
-        :type  size_gb: ``int``
-
-        :param speed: Speed of the disk (i.e. STANDARD)
-        :type  speed: ``str``
-
-        :param state: State of the disk (i.e. PENDING)
-        :type  state: ``str``
-        """
-        self.id = id
-        self.scsi_id = scsi_id
-        self.size_gb = size_gb
-        self.speed = speed
-        self.state = state
-
-    def __repr__(self):
-        return (('<DimensionDataServerDisk: '
-                 'id=%s, size_gb=%s')
-                % (self.id, self.size_gb))
-
-
-class DimensionDataServerVMWareTools(object):
-    """
-    A class that represents the VMWareTools for a node
-    """
-    def __init__(self, status, version_status, api_version):
-        """
-        Instantiate a new :class:`DimensionDataServerVMWareTools` object
-
-        :param status: The status of VMWare Tools
-        :type  status: ``str``
-
-        :param version_status: The status for the version of VMWare Tools
-            (i.e NEEDS_UPGRADE)
-        :type  version_status: ``str``
-
-        :param api_version: The API version of VMWare Tools
-        :type  api_version: ``str``
-        """
-        self.status = status
-        self.version_status = version_status
-        self.api_version = api_version
-
-    def __repr__(self):
-        return (('<DimensionDataServerVMWareTools '
-                 'status=%s, version_status=%s, '
-                 'api_version=%s>')
-                % (self.status, self.version_status, self.api_version))
-
-
-class DimensionDataFirewallRule(object):
-    """
-    DimensionData Firewall Rule for a network domain
-    """
-
-    def __init__(self, id, name, action, location, network_domain,
-                 status, ip_version, protocol, source, destination,
-                 enabled):
-        self.id = str(id)
-        self.name = name
-        self.action = action
-        self.location = location
-        self.network_domain = network_domain
-        self.status = status
-        self.ip_version = ip_version
-        self.protocol = protocol
-        self.source = source
-        self.destination = destination
-        self.enabled = enabled
-
-    def __repr__(self):
-        return (('<DimensionDataFirewallRule: id=%s, name=%s, '
-                 'action=%s, location=%s, network_domain=%s, '
-                 'status=%s, ip_version=%s, protocol=%s, source=%s, '
-                 'destination=%s, enabled=%s>')
-                % (self.id, self.name, self.action, self.location,
-                   self.network_domain, self.status, self.ip_version,
-                   self.protocol, self.source, self.destination,
-                   self.enabled))
-
-
-class DimensionDataFirewallAddress(object):
-    """
-    The source or destination model in a firewall rule
-    """
-    def __init__(self, any_ip, ip_address, ip_prefix_size,
-                 port_begin, port_end, address_list_id,
-                 port_list_id):
-        self.any_ip = any_ip
-        self.ip_address = ip_address
-        self.ip_prefix_size = ip_prefix_size
-        self.port_begin = port_begin
-        self.port_end = port_end
-        self.address_list_id = address_list_id
-        self.port_list_id = port_list_id
-
-
-class DimensionDataNatRule(object):
-    """
-    An IP NAT rule in a network domain
-    """
-    def __init__(self, id, network_domain, internal_ip, external_ip, status):
-        self.id = id
-        self.network_domain = network_domain
-        self.internal_ip = internal_ip
-        self.external_ip = external_ip
-        self.status = status
-
-    def __repr__(self):
-        return (('<DimensionDataNatRule: id=%s, status=%s>')
-                % (self.id, self.status))
-
-
-class DimensionDataAntiAffinityRule(object):
-    """
-    Anti-Affinity rule for DimensionData
-
-    An Anti-Affinity rule ensures that servers in the rule will
-    not reside on the same VMware ESX host.
-    """
-    def __init__(self, id, node_list):
-        """
-        Instantiate a new :class:`DimensionDataAntiAffinityRule`
-
-        :param id: The ID of the Anti-Affinity rule
-        :type  id: ``str``
-
-        :param node_list: List of node ids that belong in this rule
-        :type  node_list: ``list`` of ``str``
-        """
-        self.id = id
-        self.node_list = node_list
-
-    def __repr__(self):
-        return (('<DimensionDataAntiAffinityRule: id=%s>')
-                % (self.id))
-
-
-class DimensionDataVlan(object):
-    """
-    DimensionData VLAN.
-    """
-
-    def __init__(self, id, name, description, location, network_domain,
-                 status, private_ipv4_range_address, private_ipv4_range_size,
-                 ipv6_range_address, ipv6_range_size, ipv4_gateway,
-                 ipv6_gateway):
-        """
-        Initialize an instance of ``DimensionDataVlan``
-
-        :param id: The ID of the VLAN
-        :type  id: ``str``
-
-        :param name: The name of the VLAN
-        :type  name: ``str``
-
-        :param description: Plan text description of the VLAN
-        :type  description: ``str``
-
-        :param location: The location (data center) of the VLAN
-        :type  location: ``NodeLocation``
-
-        :param network_domain: The Network Domain that owns this VLAN
-        :type  network_domain: :class:`DimensionDataNetworkDomain`
-
-        :param status: The status of the VLAN
-        :type  status: :class:`DimensionDataStatus`
-
-        :param private_ipv4_range_address: The host address of the VLAN
-                                            IP space
-        :type  private_ipv4_range_address: ``str``
-
-        :param private_ipv4_range_size: The size (e.g. '24') of the VLAN
-                                            as a CIDR range size
-        :type  private_ipv4_range_size: ``int``
-
-        :param ipv6_range_address: The host address of the VLAN
-                                            IP space
-        :type  ipv6_range_address: ``str``
-
-        :param ipv6_range_size: The size (e.g. '32') of the VLAN
-                                            as a CIDR range size
-        :type  ipv6_range_size: ``int``
-
-        :param ipv4_gateway: The IPv4 default gateway address
-        :type  ipv4_gateway: ``str``
-
-        :param ipv6_gateway: The IPv6 default gateway address
-        :type  ipv6_gateway: ``str``
-        """
-        self.id = str(id)
-        self.name = name
-        self.location = location
-        self.description = description
-        self.network_domain = network_domain
-        self.status = status
-        self.private_ipv4_range_address = private_ipv4_range_address
-        self.private_ipv4_range_size = private_ipv4_range_size
-        self.ipv6_range_address = ipv6_range_address
-        self.ipv6_range_size = ipv6_range_size
-        self.ipv4_gateway = ipv4_gateway
-        self.ipv6_gateway = ipv6_gateway
-
-    def __repr__(self):
-        return (('<DimensionDataVlan: id=%s, name=%s, '
-                 'description=%s, location=%s, status=%s>')
-                % (self.id, self.name, self.description,
-                   self.location, self.status))
-
-
-class DimensionDataPool(object):
-    """
-    DimensionData VIP Pool.
-    """
-
-    def __init__(self, id, name, description, status, load_balance_method,
-                 health_monitor_id, service_down_action, slow_ramp_time):
-        """
-        Initialize an instance of ``DimensionDataPool``
-
-        :param id: The ID of the pool
-        :type  id: ``str``
-
-        :param name: The name of the pool
-        :type  name: ``str``
-
-        :param description: Plan text description of the pool
-        :type  description: ``str``
-
-        :param status: The status of the pool
-        :type  status: :class:`DimensionDataStatus`
-
-        :param load_balance_method: The load balancer method
-        :type  load_balance_method: ``str``
-
-        :param health_monitor_id: The ID of the health monitor
-        :type  health_monitor_id: ``str``
-
-        :param service_down_action: Action to take when pool is down
-        :type  service_down_action: ``str``
-
-        :param slow_ramp_time: The ramp-up time for service recovery
-        :type  slow_ramp_time: ``int``
-        """
-        self.id = str(id)
-        self.name = name
-        self.description = description
-        self.status = status
-        self.load_balance_method = load_balance_method
-        self.health_monitor_id = health_monitor_id
-        self.service_down_action = service_down_action
-        self.slow_ramp_time = slow_ramp_time
-
-    def __repr__(self):
-        return (('<DimensionDataPool: id=%s, name=%s, '
-                 'description=%s, status=%s>')
-                % (self.id, self.name, self.description,
-                   self.status))
-
-
-class DimensionDataPoolMember(object):
-    """
-    DimensionData VIP Pool Member.
-    """
-
-    def __init__(self, id, name, status, ip, port, node_id):
-        """
-        Initialize an instance of ``DimensionDataPoolMember``
-
-        :param id: The ID of the pool member
-        :type  id: ``str``
-
-        :param name: The name of the pool member
-        :type  name: ``str``
-
-        :param status: The status of the pool
-        :type  status: :class:`DimensionDataStatus`
-
-        :param ip: The IP of the pool member
-        :type  ip: ``str``
-
-        :param port: The port of the pool member
-        :type  port: ``int``
-
-        :param node_id: The ID of the associated node
-        :type  node_id: ``str``
-        """
-        self.id = str(id)
-        self.name = name
-        self.status = status
-        self.ip = ip
-        self.port = port
-        self.node_id = node_id
-
-    def __repr__(self):
-        return (('<DimensionDataPoolMember: id=%s, name=%s, '
-                 'ip=%s, status=%s, port=%s, node_id=%s>')
-                % (self.id, self.name,
-                   self.ip, self.status, self.port,
-                   self.node_id))
-
-
-class DimensionDataVIPNode(object):
-    def __init__(self, id, name, status, ip, connection_limit='10000',
-                 connection_rate_limit='10000'):
-        """
-        Initialize an instance of :class:`DimensionDataVIPNode`
-
-        :param id: The ID of the node
-        :type  id: ``str``
-
-        :param name: The name of the node
-        :type  name: ``str``
-
-        :param status: The status of the node
-        :type  status: :class:`DimensionDataStatus`
-
-        :param ip: The IP of the node
-        :type  ip: ``str``
-
-        :param connection_limit: The total connection limit for the node
-        :type  connection_limit: ``int``
-
-        :param connection_rate_limit: The rate limit for the node
-        :type  connection_rate_limit: ``int``
-        """
-        self.id = str(id)
-        self.name = name
-        self.status = status
-        self.ip = ip
-        self.connection_limit = connection_limit
-        self.connection_rate_limit = connection_rate_limit
-
-    def __repr__(self):
-        return (('<DimensionDataVIPNode: id=%s, name=%s, '
-                 'status=%s, ip=%s>')
-                % (self.id, self.name,
-                   self.status, self.ip))
-
-
-class DimensionDataVirtualListener(object):
-    """
-    DimensionData Virtual Listener.
-    """
-
-    def __init__(self, id, name, status, ip):
-        """
-        Initialize an instance of :class:`DimensionDataVirtualListener`
-
-        :param id: The ID of the listener
-        :type  id: ``str``
-
-        :param name: The name of the listener
-        :type  name: ``str``
-
-        :param status: The status of the listener
-        :type  status: :class:`DimensionDataStatus`
-
-        :param ip: The IP of the listener
-        :type  ip: ``str``
-        """
-        self.id = str(id)
-        self.name = name
-        self.status = status
-        self.ip = ip
-
-    def __repr__(self):
-        return (('<DimensionDataVirtualListener: id=%s, name=%s, '
-                 'status=%s, ip=%s>')
-                % (self.id, self.name,
-                   self.status, self.ip))
-
-
-class DimensionDataDefaultHealthMonitor(object):
-    """
-    A default health monitor for a VIP (node, pool or listener)
-    """
-    def __init__(self, id, name, node_compatible, pool_compatible):
-        """
-        Initialize an instance of :class:`DimensionDataDefaultHealthMonitor`
-
-        :param id: The ID of the monitor
-        :type  id: ``str``
-
-        :param name: The name of the monitor
-        :type  name: ``str``
-
-        :param node_compatible: Is a monitor capable of monitoring nodes
-        :type  node_compatible: ``bool``
-
-        :param pool_compatible: Is a monitor capable of monitoring pools
-        :type  pool_compatible: ``bool``
-        """
-        self.id = id
-        self.name = name
-        self.node_compatible = node_compatible
-        self.pool_compatible = pool_compatible
-
-    def __repr__(self):
-        return (('<DimensionDataDefaultHealthMonitor: id=%s, name=%s>')
-                % (self.id, self.name))
-
-
-class DimensionDataPersistenceProfile(object):
-    """
-    Each Persistence Profile declares the combination of Virtual Listener
-    type and protocol with which it is
-    compatible and whether or not it is compatible as a
-    Fallback Persistence Profile.
-    """
-    def __init__(self, id, name, compatible_listeners, fallback_compatible):
-        """
-        Initialize an instance of :class:`DimensionDataPersistenceProfile`
-
-        :param id: The ID of the profile
-        :type  id: ``str``
-
-        :param name: The name of the profile
-        :type  name: ``str``
-
-        :param compatible_listeners: List of compatible Virtual Listener types
-        :type  compatible_listeners: ``list`` of
-            :class:`DimensionDataVirtualListenerCompatibility`
-
-        :param fallback_compatible: Is capable as a fallback profile
-        :type  fallback_compatible: ``bool``
-        """
-        self.id = id
-        self.name = name
-        self.compatible_listeners = compatible_listeners
-        self.fallback_compatible = fallback_compatible
-
-    def __repr__(self):
-        return (('<DimensionDataPersistenceProfile: id=%s, name=%s>')
-                % (self.id, self.name))
-
-
-class DimensionDataDefaultiRule(object):
-    """
-    A default iRule for a network domain, can be applied to a listener
-    """
-    def __init__(self, id, name, compatible_listeners):
-        """
-        Initialize an instance of :class:`DimensionDataDefaultiRule`
-
-        :param id: The ID of the iRule
-        :type  id: ``str``
-
-        :param name: The name of the iRule
-        :type  name: ``str``
-
-        :param compatible_listeners: List of compatible Virtual Listener types
-        :type  compatible_listeners: ``list`` of
-            :class:`DimensionDataVirtualListenerCompatibility`
-        """
-        self.id = id
-        self.name = name
-        self.compatible_listeners = compatible_listeners
-
-    def __repr__(self):
-        return (('<DimensionDataDefaultiRule: id=%s, name=%s>')
-                % (self.id, self.name))
-
-
-class DimensionDataVirtualListenerCompatibility(object):
-    """
-    A compatibility preference for a persistence profile or iRule
-    specifies which virtual listener types this profile or iRule can be
-    applied to.
-    """
-    def __init__(self, type, protocol):
-        self.type = type
-        self.protocol = protocol
-
-    def __repr__(self):
-        return (('<DimensionDataVirtualListenerCompatibility: '
-                 'type=%s, protocol=%s>')
-                % (self.type, self.protocol))
-
-
-class DimensionDataBackupDetails(object):
-    """
-    Dimension Data Backup Details represents information about
-    a targets backups configuration
-    """
-
-    def __init__(self, asset_id, service_plan, status, clients=None):
-        """
-        Initialize an instance of :class:`DimensionDataBackupDetails`
-
-        :param asset_id: Asset identification for backups
-        :type  asset_id: ``str``
-
-        :param service_plan: The service plan for backups. i.e (Essentials)
-        :type  service_plan: ``str``
-
-        :param status: The overall status this backup target.
-                       i.e. (unregistered)
-        :type  status: ``str``
-
-        :param clients: Backup clients attached to this target
-        :type  clients: ``list`` of :class:`DimensionDataBackupClient`
-        """
-        self.asset_id = asset_id
-        self.service_plan = service_plan
-        self.status = status
-        self.clients = clients
-
-    def __repr__(self):
-        return (('<DimensionDataBackupDetails: id=%s>')
-                % (self.asset_id))
-
-
-class DimensionDataBackupClient(object):
-    """
-    An object that represents a backup client
-    """
-    def __init__(self, id, type, status,
-                 schedule_policy, storage_policy, download_url,
-                 alert=None, running_job=None):
-        """
-        Initialize an instance of :class:`DimensionDataBackupClient`
-
-        :param id: Unique ID for the client
-        :type  id: ``str``
-
-        :param type: The type of client that this client is
-        :type  type: :class:`DimensionDataBackupClientType`
-
-        :param status: The states of this particular backup client.
-                       i.e. (Unregistered)
-        :type  status: ``str``
-
-        :param schedule_policy: The schedule policy for this client
-                                NOTE: Dimension Data only sends back the name
-                                of the schedule policy, no further details
-        :type  schedule_policy: ``str``
-
-        :param storage_policy: The storage policy for this client
-                               NOTE: Dimension Data only sends back the name
-                               of the storage policy, no further details
-        :type  storage_policy: ``str``
-
-        :param download_url: The download url for this client
-        :type  download_url: ``str``
-
-        :param alert: The alert configured for this backup client (optional)
-        :type  alert: :class:`DimensionDataBackupClientAlert`
-
-        :param alert: The running job for the client (optional)
-        :type  alert: :class:`DimensionDataBackupClientRunningJob`
-        """
-        self.id = id
-        self.type = type
-        self.status = status
-        self.schedule_policy = schedule_policy
-        self.storage_policy = storage_policy
-        self.download_url = download_url
-        self.alert = alert
-        self.running_job = running_job
-
-    def __repr__(self):
-        return (('<DimensionDataBackupClient: id=%s>')
-                % (self.id))
-
-
-class DimensionDataBackupClientAlert(object):
-    """
-    An alert for a backup client
-    """
-    def __init__(self, trigger, notify_list=[]):
-        """
-        Initialize an instance of :class:`DimensionDataBackupClientAlert`
-
-        :param trigger: Trigger type for the client i.e. ON_FAILURE
-        :type  trigger: ``str``
-
-        :param notify_list: List of email addresses that are notified
-                            when the alert is fired
-        :type  notify_list: ``list`` of ``str``
-        """
-        self.trigger = trigger
-        self.notify_list = notify_list
-
-    def __repr__(self):
-        return (('<DimensionDataBackupClientAlert: trigger=%s>')
-                % (self.trigger))
-
-
-class DimensionDataBackupClientRunningJob(object):
-    """
-    A running job for a given backup client
-    """
-    def __init__(self, id, status, percentage=0):
-        """
-        Initialize an instance of :class:`DimensionDataBackupClientRunningJob`
-
-        :param id: The unqiue ID of the job
-        :type  id: ``str``
-
-        :param status: The status of the job i.e. Waiting
-        :type  status: ``str``
-
-        :param percentage: The percentage completion of the job
-        :type  percentage: ``int``
-        """
-        self.id = id
-        self.percentage = percentage
-        self.status = status
-
-    def __repr__(self):
-        return (('<DimensionDataBackupClientRunningJob: id=%s>')
-                % (self.id))
-
-
-class DimensionDataBackupClientType(object):
-    """
-    A client type object for backups
-    """
-    def __init__(self, type, is_file_system, description):
-        """
-        Initialize an instance of :class:`DimensionDataBackupClientType`
-
-        :param type: The type of client i.e. (FA.Linux, MySQL, ect.)
-        :type  type: ``str``
-
-        :param is_file_system: The name of the iRule
-        :type  is_file_system: ``bool``
-
-        :param description: Description of the client
-        :type  description: ``str``
-        """
-        self.type = type
-        self.is_file_system = is_file_system
-        self.description = description
-
-    def __repr__(self):
-        return (('<DimensionDataBackupClientType: type=%s>')
-                % (self.type))
-
-
-class DimensionDataBackupStoragePolicy(object):
-    """
-    A representation of a storage policy
-    """
-    def __init__(self, name, retention_period, secondary_location):
-        """
-        Initialize an instance of :class:`DimensionDataBackupStoragePolicy`
-
-        :param name: The name of the storage policy i.e. 14 Day Storage Policy
-        :type  name: ``str``
-
-        :param retention_period: How long to keep the backup in days
-        :type  retention_period: ``int``
-
-        :param secondary_location: The secondary location i.e. Primary
-        :type  secondary_location: ``str``
-        """
-        self.name = name
-        self.retention_period = retention_period
-        self.secondary_location = secondary_location
-
-    def __repr__(self):
-        return (('<DimensionDataBackupStoragePolicy: name=%s>')
-                % (self.name))
-
-
-class DimensionDataBackupSchedulePolicy(object):
-    """
-    A representation of a schedule policy
-    """
-    def __init__(self, name, description):
-        """
-        Initialize an instance of :class:`DimensionDataBackupSchedulePolicy`
-
-        :param name: The name of the policy i.e 12AM - 6AM
-        :type  name: ``str``
-
-        :param description: Short summary of the details of the policy
-        :type  description: ``str``
-        """
-        self.name = name
-        self.description = description
-
-    def __repr__(self):
-        return (('<DimensionDataBackupSchedulePolicy: name=%s>')
-                % (self.name))
-
-
-class DimensionDataTag(object):
-    """
-    A representation of a Tag in Dimension Data
-    A Tag first must have a Tag Key, then an asset is tag with
-    a key and an option value.  Tags can be queried later to filter assets
-    and also show up on usage report if so desired.
-    """
-    def __init__(self, asset_type, asset_id, asset_name,
-                 datacenter, key, value):
-        """
-        Initialize an instance of :class:`DimensionDataTag`
-
-        :param asset_type: The type of asset.  Current asset types:
-                           SERVER, VLAN, NETWORK_DOMAIN, CUSTOMER_IMAGE,
-                           PUBLIC_IP_BLOCK, ACCOUNT
-        :type  asset_type: ``str``
-
-        :param asset_id: The GUID of the asset that is tagged
-        :type  asset_id: ``str``
-
-        :param asset_name: The name of the asset that is tagged
-        :type  asset_name: ``str``
-
-        :param datacenter: The short datacenter name of the tagged asset
-        :type  datacenter: ``str``
-
-        :param key: The tagged key
-        :type  key: :class:`DimensionDataTagKey`
-
-        :param value: The tagged value
-        :type  value: ``None`` or ``str``
-        """
-        self.asset_type = asset_type
-        self.asset_id = asset_id
-        self.asset_name = asset_name
-        self.datacenter = datacenter
-        self.key = key
-        self.value = value
-
-    def __repr__(self):
-        return (('<DimensionDataTag: asset_name=%s, tag_name=%s, value=%s>')
-                % (self.asset_name, self.key.name, self.value))
-
-
-class DimensionDataTagKey(object):
-    """
-    A representation of a Tag Key in Dimension Data
-    A tag key is required to tag an asset
-    """
-    def __init__(self, id, name, description,
-                 value_required, display_on_report):
-        """
-        Initialize an instance of :class:`DimensionDataTagKey`
-
-        :param id: GUID of the tag key
-        :type  id: ``str``
-
-        :param name: Name of the tag key
-        :type  name: ``str``
-
-        :param description: Description of the tag key
-        :type  description: ``str``
-
-        :param value_required: If a value is required for this tag key
-        :type  value_required: ``bool``
-
-        :param display_on_report: If this tag key should be displayed on
-                                  usage reports
-        :type  display_on_report: ``bool``
-        """
-        self.id = id
-        self.name = name
-        self.description = description
-        self.value_required = value_required
-        self.display_on_report = display_on_report
-
-    def __repr__(self):
-        return (('<DimensionDataTagKey: name=%s>')
-                % (self.name))
+# 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.
+"""
+Dimension Data Common Components
+"""
+from base64 import b64encode
+from time import sleep
+from libcloud.utils.py3 import httplib
+from libcloud.utils.py3 import b
+from libcloud.common.base import ConnectionUserAndKey, XmlResponse, RawResponse
+from libcloud.common.types import LibcloudError, InvalidCredsError
+from libcloud.compute.base import Node
+from libcloud.utils.py3 import basestring
+from libcloud.utils.xml import findtext
+
+# Roadmap / TODO:
+#
+# 1.0 - Copied from OpSource API, named provider details.
+
+# setup a few variables to represent all of the DimensionData cloud namespaces
+NAMESPACE_BASE = "http://oec.api.opsource.net/schemas"
+ORGANIZATION_NS = NAMESPACE_BASE + "/organization"
+SERVER_NS = NAMESPACE_BASE + "/server"
+NETWORK_NS = NAMESPACE_BASE + "/network"
+DIRECTORY_NS = NAMESPACE_BASE + "/directory"
+GENERAL_NS = NAMESPACE_BASE + "/general"
+BACKUP_NS = NAMESPACE_BASE + "/backup"
+
+# API 2.0 Namespaces and URNs
+TYPES_URN = "urn:didata.com:api:cloud:types"
+
+# API end-points
+API_ENDPOINTS = {
+    'dd-na': {
+        'name': 'North America (NA)',
+        'host': 'api-na.dimensiondata.com',
+        'vendor': 'DimensionData'
+    },
+    'dd-eu': {
+        'name': 'Europe (EU)',
+        'host': 'api-eu.dimensiondata.com',
+        'vendor': 'DimensionData'
+    },
+    'dd-au': {
+        'name': 'Australia (AU)',
+        'host': 'api-au.dimensiondata.com',
+        'vendor': 'DimensionData'
+    },
+    'dd-au-gov': {
+        'name': 'Australia Canberra ACT (AU)',
+        'host': 'api-canberra.dimensiondata.com',
+        'vendor': 'DimensionData'
+    },
+    'dd-af': {
+        'name': 'Africa (AF)',
+        'host': 'api-mea.dimensiondata.com',
+        'vendor': 'DimensionData'
+    },
+    'dd-ap': {
+        'name': 'Asia Pacific (AP)',
+        'host': 'api-ap.dimensiondata.com',
+        'vendor': 'DimensionData'
+    },
+    'dd-latam': {
+        'name': 'South America (LATAM)',
+        'host': 'api-latam.dimensiondata.com',
+        'vendor': 'DimensionData'
+    },
+    'dd-canada': {
+        'name': 'Canada (CA)',
+        'host': 'api-canada.dimensiondata.com',
+        'vendor': 'DimensionData'
+    },
+    'is-na': {
+        'name': 'North America (NA)',
+        'host': 'usapi.cloud.is.co.za',
+        'vendor': 'InternetSolutions'
+    },
+    'is-eu': {
+        'name': 'Europe (EU)',
+        'host': 'euapi.cloud.is.co.za',
+        'vendor': 'InternetSolutions'
+    },
+    'is-au': {
+        'name': 'Australia (AU)',
+        'host': 'auapi.cloud.is.co.za',
+        'vendor': 'InternetSolutions'
+    },
+    'is-af': {
+        'name': 'Africa (AF)',
+        'host': 'meaapi.cloud.is.co.za',
+        'vendor': 'InternetSolutions'
+    },
+    'is-ap': {
+        'name': 'Asia Pacific (AP)',
+        'host': 'apapi.cloud.is.co.za',
+        'vendor': 'InternetSolutions'
+    },
+    'is-latam': {
+        'name': 'South America (LATAM)',
+        'host': 'latamapi.cloud.is.co.za',
+        'vendor': 'InternetSolutions'
+    },
+    'is-canada': {
+        'name': 'Canada (CA)',
+        'host': 'canadaapi.cloud.is.co.za',
+        'vendor': 'InternetSolutions'
+    },
+    'ntta-na': {
+        'name': 'North America (NA)',
+        'host': 'cloudapi.nttamerica.com',
+        'vendor': 'NTTNorthAmerica'
+    },
+    'ntta-eu': {
+        'name': 'Europe (EU)',
+        'host': 'eucloudapi.nttamerica.com',
+        'vendor': 'NTTNorthAmerica'
+    },
+    'ntta-au': {
+        'name': 'Australia (AU)',
+        'host': 'aucloudapi.nttamerica.com',
+        'vendor': 'NTTNorthAmerica'
+    },
+    'ntta-af': {
+        'name': 'Africa (AF)',
+        'host': 'sacloudapi.nttamerica.com',
+        'vendor': 'NTTNorthAmerica'
+    },
+    'ntta-ap': {
+        'name': 'Asia Pacific (AP)',
+        'host': 'hkcloudapi.nttamerica.com',
+        'vendor': 'NTTNorthAmerica'
+    },
+    'cisco-na': {
+        'name': 'North America (NA)',
+        'host': 'iaas-api-na.cisco-ccs.com',
+        'vendor': 'Cisco'
+    },
+    'cisco-eu': {
+        'name': 'Europe (EU)',
+        'host': 'iaas-api-eu.cisco-ccs.com',
+        'vendor': 'Cisco'
+    },
+    'cisco-au': {
+        'name': 'Australia (AU)',
+        'host': 'iaas-api-au.cisco-ccs.com',
+        'vendor': 'Cisco'
+    },
+    'cisco-af': {
+        'name': 'Africa (AF)',
+        'host': 'iaas-api-mea.cisco-ccs.com',
+        'vendor': 'Cisco'
+    },
+    'cisco-ap': {
+        'name': 'Asia Pacific (AP)',
+        'host': 'iaas-api-ap.cisco-ccs.com',
+        'vendor': 'Cisco'
+    },
+    'cisco-latam': {
+        'name': 'South America (LATAM)',
+        'host': 'iaas-api-sa.cisco-ccs.com',
+        'vendor': 'Cisco'
+    },
+    'cisco-canada': {
+        'name': 'Canada (CA)',
+        'host': 'iaas-api-ca.cisco-ccs.com',
+        'vendor': 'Cisco'
+    },
+    'med1-il': {
+        'name': 'Israel (IL)',
+        'host': 'api.cloud.med-1.com',
+        'vendor': 'Med-1'
+    },
+    'med1-na': {
+        'name': 'North America (NA)',
+        'host': 'api-na.cloud.med-1.com',
+        'vendor': 'Med-1'
+    },
+    'med1-eu': {
+        'name': 'Europe (EU)',
+        'host': 'api-eu.cloud.med-1.com',
+        'vendor': 'Med-1'
+    },
+    'med1-au': {
+        'name': 'Australia (AU)',
+        'host': 'api-au.cloud.med-1.com',
+        'vendor': 'Med-1'
+    },
+    'med1-af': {
+        'name': 'Africa (AF)',
+        'host': 'api-af.cloud.med-1.com',
+        'vendor': 'Med-1'
+    },
+    'med1-ap': {
+        'name': 'Asia Pacific (AP)',
+        'host': 'api-ap.cloud.med-1.com',
+        'vendor': 'Med-1'
+    },
+    'med1-latam': {
+        'name': 'South America (LATAM)',
+        'host': 'api-sa.cloud.med-1.com',
+        'vendor': 'Med-1'
+    },
+    'med1-canada': {
+        'name': 'Canada (CA)',
+        'host': 'api-ca.cloud.med-1.com',
+        'vendor': 'Med-1'
+    },
+    'indosat-id': {
+        'name': 'Indonesia (ID)',
+        'host': 'iaas-api.indosat.com',
+        'vendor': 'Indosat'
+    },
+    'indosat-na': {
+        'name': 'North America (NA)',
+        'host': 'iaas-usapi.indosat.com',
+        'vendor': 'Indosat'
+    },
+    'indosat-eu': {
+        'name': 'Europe (EU)',
+        'host': 'iaas-euapi.indosat.com',
+        'vendor': 'Indosat'
+    },
+    'indosat-au': {
+        'name': 'Australia (AU)',
+        'host': 'iaas-auapi.indosat.com',
+        'vendor': 'Indosat'
+    },
+    'indosat-af': {
+        'name': 'Africa (AF)',
+        'host': 'iaas-afapi.indosat.com',
+        'vendor': 'Indosat'
+    },
+    'bsnl-in': {
+        'name': 'India (IN)',
+        'host': 'api.bsnlcloud.com',
+        'vendor': 'BSNL'
+    },
+    'bsnl-na': {
+        'name': 'North America (NA)',
+        'host': 'usapi.bsnlcloud.com',
+        'vendor': 'BSNL'
+    },
+    'bsnl-eu': {
+        'name': 'Europe (EU)',
+        'host': 'euapi.bsnlcloud.com',
+        'vendor': 'BSNL'
+    },
+    'bsnl-au': {
+        'name': 'Australia (AU)',
+        'host': 'auapi.bsnlcloud.com',
+        'vendor': 'BSNL'
+    },
+    'bsnl-af': {
+        'name': 'Africa (AF)',
+        'host': 'afapi.bsnlcloud.com',
+        'vendor': 'BSNL'
+    },
+}
+
+# Default API end-point for the base connection class.
+DEFAULT_REGION = 'dd-na'
+
+BAD_CODE_XML_ELEMENTS = (
+    ('responseCode', SERVER_NS),
+    ('responseCode', TYPES_URN),
+    ('result', GENERAL_NS)
+)
+
+BAD_MESSAGE_XML_ELEMENTS = (
+    ('message', SERVER_NS),
+    ('message', TYPES_URN),
+    ('resultDetail', GENERAL_NS)
+)
+
+
+def dd_object_to_id(obj, obj_type, id_value='id'):
+    """
+    Takes in a DD object or string and prints out it's id
+    This is a helper method, as many of our functions can take either an object
+    or a string, and we need an easy way of converting them
+
+    :param obj: The object to get the id for
+    :type  obj: ``object``
+
+    :param  func: The function to call, e.g. ex_get_vlan. Note: This
+                  function needs to return an object which has ``status``
+                  attribute.
+    :type   func: ``function``
+
+    :rtype: ``str``
+    """
+    if isinstance(obj, obj_type):
+        return getattr(obj, id_value)
+    elif isinstance(obj, (basestring)):
+        return obj
+    else:
+        raise TypeError(
+            "Invalid type %s looking for basestring or %s"
+            % (type(obj).__name__, obj_type.__name__)
+        )
+
+
+class NetworkDomainServicePlan(object):
+    ESSENTIALS = "ESSENTIALS"
+    ADVANCED = "ADVANCED"
+
+
+class DimensionDataRawResponse(RawResponse):
+    pass
+
+
+class DimensionDataResponse(XmlResponse):
+    def parse_error(self):
+        if self.status == httplib.UNAUTHORIZED:
+            raise InvalidCredsError(self.body)
+        elif self.status == httplib.FORBIDDEN:
+            raise InvalidCredsError(self.body)
+
+        body = self.parse_body()
+
+        if self.status == httplib.BAD_REQUEST:
+            for response_code in BAD_CODE_XML_ELEMENTS:
+                code = findtext(body, response_code[0], response_code[1])
+                if code is not None:
+                    break
+            for message in BAD_MESSAGE_XML_ELEMENTS:
+                message = findtext(body, message[0], message[1])
+                if message is not None:
+                    break
+            raise DimensionDataAPIException(code=code,
+                                            msg=message,
+                                            driver=self.connection.driver)
+        if self.status is not httplib.OK:
+            raise DimensionDataAPIException(code=self.status,
+                                            msg=body,
+                                            driver=self.connection.driver)
+
+        return self.body
+
+
+class DimensionDataAPIException(LibcloudError):
+    def __init__(self, code, msg, driver):
+        self.code = code
+        self.msg = msg
+        self.driver = driver
+
+    def __str__(self):
+        return "%s: %s" % (self.code, self.msg)
+
+    def __repr__(self):
+        return ("<DimensionDataAPIException: code='%s', msg='%s'>" %
+                (self.code, self.msg))
+
+
+class DimensionDataConnection(ConnectionUserAndKey):
+    """
+    Connection class for the DimensionData driver
+    """
+
+    api_path_version_1 = '/oec'
+    api_path_version_2 = '/caas'
+    api_version_1 = '0.9'
+    api_version_2 = '2.2'
+
+    _orgId = None
+    responseCls = DimensionDataResponse
+    rawResponseCls = DimensionDataRawResponse
+
+    allow_insecure = False
+
+    def __init__(self, user_id, key, secure=True, host=None, port=None,
+                 url=None, timeout=None, proxy_url=None, **conn_kwargs):
+        super(DimensionDataConnection, self).__init__(
+            user_id=user_id,
+            key=key,
+            secure=secure,
+            host=host, port=port,
+            url=url, timeout=timeout,
+            proxy_url=proxy_url)
+
+        if conn_kwargs['region']:
+            self.host = conn_kwargs['region']['host']
+
+    def add_default_headers(self, headers):
+        headers['Authorization'] = \
+            ('Basic %s' % b64encode(b('%s:%s' % (self.user_id,
+                                                 self.key))).decode('utf-8'))
+        headers['Content-Type'] = 'application/xml'
+        return headers
+
+    def request_api_1(self, action, params=None, data='',
+                      headers=None, method='GET'):
+        action = "%s/%s/%s" % (self.api_path_version_1,
+                               self.api_version_1, action)
+
+        return super(DimensionDataConnection, self).request(
+            action=action,
+            params=params, data=data,
+            method=method, headers=headers)
+
+    def request_api_2(self, path, action, params=None, data='',
+                      headers=None, method='GET'):
+        action = "%s/%s/%s/%s" % (self.api_path_version_2,
+                                  self.api_version_2, path, action)
+
+        return super(DimensionDataConnection, self).request(
+            action=action,
+            params=params, data=data,
+            method=method, headers=headers)
+
+    def raw_request_with_orgId_api_1(self, action, params=None, data='',
+                                     headers=None, method='GET'):
+        action = "%s/%s" % (self.get_resource_path_api_1(), action)
+        return super(DimensionDataConnection, self).request(
+            action=action,
+            params=params, data=data,
+            method=method, headers=headers, raw=True)
+
+    def request_with_orgId_api_1(self, action, params=None, data='',
+                                 headers=None, method='GET'):
+        action = "%s/%s" % (self.get_resource_path_api_1(), action)
+
+        return super(DimensionDataConnection, self).request(
+            action=action,
+            params=params, data=data,
+            method=method, headers=headers)
+
+    def request_with_orgId_api_2(self, action, params=None, data='',
+                                 headers=None, method='GET'):
+        action = "%s/%s" % (self.get_resource_path_api_2(), action)
+
+        return super(DimensionDataConnection, self).request(
+            action=action,
+            params=params, data=data,
+            method=method, headers=headers)
+
+    def paginated_request_with_orgId_api_2(self, action, params=None, data='',
+                                           headers=None, method='GET',
+                                           page_size=250):
+        """
+        A paginated request to the MCP2.0 API
+        This essentially calls out to request_with_orgId_api_2 for each page
+        and yields the response to make a generator
+        This generator can be looped through to grab all the pages.
+
+        :param action: The resource to access (i.e. 'network/vlan')
+        :type  action: ``str``
+
+        :param params: Parameters to give to the action
+        :type  params: ``dict`` or ``None``
+
+        :param data: The data payload to be added to the request
+        :type  data: ``str``
+
+        :param headers: Additional header to be added to the request
+        :type  headers: ``str`` or ``dict`` or ``None``
+
+        :param method: HTTP Method for the request (i.e. 'GET', 'POST')
+        :type  method: ``str``
+
+        :param page_size: The size of each page to be returned
+                          Note: Max page size in MCP2.0 is currently 250
+        :type  page_size: ``int``
+        """
+        if params is None:
+            params = {}
+        params['pageSize'] = page_size
+
+        paged_resp = self.request_with_orgId_api_2(action, params,
+                                                   data, headers,
+                                                   method).object
+        yield paged_resp
+        paged_resp = paged_resp or {}
+
+        while int(paged_resp.get('pageCount')) >= \
+                int(paged_resp.get('pageSize')):
+            params['pageNumber'] = int(paged_resp.get('pageNumber')) + 1
+            paged_resp = self.request_with_orgId_api_2(action, params,
+                                                       data, headers,
+                                                       method).object
+            yield paged_resp
+
+    def get_resource_path_api_1(self):
+        """
+        This method returns a resource path which is necessary for referencing
+        resources that require a full path instead of just an ID, such as
+        networks, and customer snapshots.
+        """
+        return ("%s/%s/%s" % (self.api_path_version_1, self.api_version_1,
+                              self._get_orgId()))
+
+    def get_resource_path_api_2(self):
+        """
+        This method returns a resource path which is necessary for referencing
+        resources that require a full path instead of just an ID, such as
+        networks, and customer snapshots.
+        """
+        return ("%s/%s/%s" % (self.api_path_version_2, self.api_version_2,
+                              self._get_orgId()))
+
+    def wait_for_state(self, state, func, poll_interval=2, timeout=60, *args,
+                       **kwargs):
+        """
+        Wait for the function which returns a instance with field status/state
+        to match.
+
+        Keep polling func until one of the desired states is matched
+
+        :param state: Either the desired state (`str`) or a `list` of states
+        :type  state: ``str`` or ``list``
+
+        :param  func: The function to call, e.g. ex_get_vlan. Note: This
+                      function needs to return an object which has ``status``
+                      attribute.
+        :type   func: ``function``
+
+        :param  poll_interval: The number of seconds to wait between checks
+        :type   poll_interval: `int`
+
+        :param  timeout: The total number of seconds to wait to reach a state
+        :type   timeout: `int`
+
+        :param  args: The arguments for func
+        :type   args: Positional arguments
+
+        :param  kwargs: The arguments for func
+        :type   kwargs: Keyword arguments
+
+        :return: Result from the calling function.
+        """
+        cnt = 0
+        while cnt < timeout / poll_interval:
+            result = func(*args, **kwargs)
+            if isinstance(result, Node):
+                object_state = result.state
+            else:
+                object_state = result.status
+
+            if object_state is state or object_state in state:
+                return result
+            sleep(poll_interval)
+            cnt += 1
+
+        msg = 'Status check for object %s timed out' % (result)
+        raise DimensionDataAPIException(code=object_state,
+                                        msg=msg,
+                                        driver=self.driver)
+
+    def _get_orgId(self):
+        """
+        Send the /myaccount API request to DimensionData cloud and parse the
+        'orgId' from the XML response object. We need the orgId to use most
+        of the other API functions
+        """
+        if self._orgId is None:
+            body = self.request_api_1('myaccount').object
+            self._orgId = findtext(body, 'orgId', DIRECTORY_NS)
+        return self._orgId
+
+    def get_account_details(self):
+        """
+        Get the details of this account
+
+        :rtype: :class:`DimensionDataAccountDetails`
+        """
+        body = self.request_api_1('myaccount').object
+        return DimensionDataAccountDetails(
+            user_name=findtext(body, 'userName', DIRECTORY_NS),
+            full_name=findtext(body, 'fullName', DIRECTORY_NS),
+            first_name=findtext(body, 'firstName', DIRECTORY_NS),
+            last_name=findtext(body, 'lastName', DIRECTORY_NS),
+            email=findtext(body, 'emailAddress', DIRECTORY_NS))
+
+
+class DimensionDataAccountDetails(object):
+    """
+    Dimension Data account class details
+    """
+    def __init__(self, user_name, full_name, first_name, last_name, email):
+        self.user_name = user_name
+        self.full_name = full_name
+        self.first_name = first_name
+        self.last_name = last_name
+        self.email = email
+
+
+class DimensionDataStatus(object):
+    """
+    DimensionData API pending operation status class
+        action, request_time, user_name, number_of_steps, update_time,
+        step.name, step.number, step.percent_complete, failure_reason,
+    """
+    def __init__(self, action=None, request_time=None, user_name=None,
+                 number_of_steps=None, update_time=None, step_name=None,
+                 step_number=None, step_percent_complete=None,
+                 failure_reason=None):
+        self.action = action
+        self.request_time = request_time
+        self.user_name = user_name
+        self.number_of_steps = number_of_steps
+        self.update_time = update_time
+        self.step_name = step_name
+        self.step_number = step_number
+        self.step_percent_complete = step_percent_complete
+        self.failure_reason = failure_reason
+
+    def __repr__(self):
+        return (('<DimensionDataStatus: action=%s, request_time=%s, '
+                 'user_name=%s, number_of_steps=%s, update_time=%s, '
+                 'step_name=%s, step_number=%s, '
+                 'step_percent_complete=%s, failure_reason=%s>')
+                % (self.action, self.request_time, self.user_name,
+                   self.number_of_steps, self.update_time, self.step_name,
+                   self.step_number, self.step_percent_complete,
+                   self.failure_reason))
+
+
+class DimensionDataNetwork(object):
+    """
+    DimensionData network with location.
+    """
+
+    def __init__(self, id, name, description, location, private_net,
+                 multicast, status):
+        self.id = str(id)
+        self.name = name
+        self.description = description
+        self.location = location
+        self.private_net = private_net
+        self.multicast = multicast
+        self.status = status
+
+    def __repr__(self):
+        return (('<DimensionDataNetwork: id=%s, name=%s, description=%s, '
+                 'location=%s, private_net=%s, multicast=%s>')
+                % (self.id, self.name, self.description, self.location,
+                   self.private_net, self.multicast))
+
+
+class DimensionDataNetworkDomain(object):
+    """
+    DimensionData network domain with location.
+    """
+
+    def __init__(self, id, name, description, location, status, plan):
+        self.id = str(id)
+        self.name = name
+        self.description = description
+        self.location = location
+        self.status = status
+        self.plan = plan
+
+    def __repr__(self):
+        return (('<DimensionDataNetworkDomain: id=%s, name=%s, '
+                 'description=%s, location=%s, status=%s>')
+                % (self.id, self.name, self.description, self.location,
+                   self.status))
+
+
+class DimensionDataPublicIpBlock(object):
+    """
+    DimensionData Public IP Block with location.
+    """
+
+    def __init__(self, id, base_ip, size, location, network_domain,
+                 status):
+        self.id = str(id)
+        self.base_ip = base_ip
+        self.size = size
+        self.location = location
+        self.network_domain = network_domain
+        self.status = status
+
+    def __repr__(self):
+        return (('<DimensionDataNetworkDomain: id=%s, base_ip=%s, '
+                 'size=%s, location=%s, status=%s>')
+                % (self.id, self.base_ip, self.size, self.location,
+                   self.status))
+
+
+class DimensionDataServerCpuSpecification(object):
+    """
+    A class that represents the specification of the CPU(s) for a
+    node
+    """
+    def __init__(self, cpu_count, cores_per_socket, performance):
+        """
+        Instantiate a new :class:`DimensionDataServerCpuSpecification`
+
+        :param cpu_count: The number of CPUs
+        :type  cpu_count: ``int``
+
+        :param cores_per_socket: The number of cores per socket, the
+            recommendation is 1
+        :type  cores_per_socket: ``int``
+
+        :param performance: The performance type, e.g. HIGHPERFORMANCE
+        :type  performance: ``str``
+        """
+        self.cpu_count = cpu_count
+        self.cores_per_socket = cores_per_socket
+        self.performance = performance
+
+    def __repr__(self):
+        return (('<DimensionDataServerCpuSpecification: '
+                 'cpu_count=%s, cores_per_socket=%s, '
+                 'performance=%s>')
+                % (self.cpu_count, self.cores_per_socket, self.performance))
+
+
+class DimensionDataServerDisk(object):
+    """
+    A class that represents the disk on a server
+    """
+    def __init__(self, id, scsi_id, size_gb, speed, state):
+        """
+        Instantiate a new :class:`DimensionDataServerDisk`
+
+        :param id: The id of the disk
+        :type  id: ``str``
+
+        :param scsi_id: Representation for scsi
+        :type  scsi_id: ``int``
+
+        :param size_gb: Size of the disk
+        :type  size_gb: ``int``
+
+        :param speed: Speed of the disk (i.e. STANDARD)
+        :type  speed: ``str``
+
+        :param state: State of the disk (i.e. PENDING)
+        :type  state: ``str``
+        """
+        self.id = id
+        self.scsi_id = scsi_id
+        self.size_gb = size_gb
+        self.speed = speed
+        self.state = state
+
+    def __repr__(self):
+        return (('<DimensionDataServerDisk: '
+                 'id=%s, size_gb=%s')
+                % (self.id, self.size_gb))
+
+
+class DimensionDataServerVMWareTools(object):
+    """
+    A class that represents the VMWareTools for a node
+    """
+    def __init__(self, status, version_status, api_version):
+        """
+        Instantiate a new :class:`DimensionDataServerVMWareTools` object
+
+        :param status: The status of VMWare Tools
+        :type  status: ``str``
+
+        :param version_status: The status for the version of VMWare Tools
+            (i.e NEEDS_UPGRADE)
+        :type  version_status: ``str``
+
+        :param api_version: The API version of VMWare Tools
+        :type  api_version: ``str``
+        """
+        self.status = status
+        self.version_status = version_status
+        self.api_version = api_version
+
+    def __repr__(self):
+        return (('<DimensionDataServerVMWareTools '
+                 'status=%s, version_status=%s, '
+                 'api_version=%s>')
+                % (self.status, self.version_status, self.api_version))
+
+
+class DimensionDataFirewallRule(object):
+    """
+    DimensionData Firewall Rule for a network domain
+    """
+
+    def __init__(self, id, name, action, location, network_domain,
+                 status, ip_version, protocol, source, destination,
+                 enabled):
+        self.id = str(id)
+        self.name = name
+        self.action = action
+        self.location = location
+        self.network_domain = network_domain
+        self.status = status
+        self.ip_version = ip_version
+        self.protocol = protocol
+        self.source = source
+        self.destination = destination
+        self.enabled = enabled
+
+    def __repr__(self):
+        return (('<DimensionDataFirewallRule: id=%s, name=%s, '
+                 'action=%s, location=%s, network_domain=%s, '
+                 'status=%s, ip_version=%s, protocol=%s, source=%s, '
+                 'destination=%s, enabled=%s>')
+                % (self.id, self.name, self.action, self.location,
+                   self.network_domain, self.status, self.ip_version,
+                   self.protocol, self.source, self.destination,
+                   self.enabled))
+
+
+class DimensionDataFirewallAddress(object):
+    """
+    The source or destination model in a firewall rule
+    """
+    def __init__(self, any_ip, ip_address, ip_prefix_size,
+                 port_begin, port_end, address_list_id,
+                 port_list_id):
+        self.any_ip = any_ip
+        self.ip_address = ip_address
+        self.ip_prefix_size = ip_prefix_size
+        self.port_begin = port_begin
+        self.port_end = port_end
+        self.address_list_id = address_list_id
+        self.port_list_id = port_list_id
+
+
+class DimensionDataNatRule(object):
+    """
+    An IP NAT rule in a network domain
+    """
+    def __init__(self, id, network_domain, internal_ip, external_ip, status):
+        self.id = id
+        self.network_domain = network_domain
+        self.internal_ip = internal_ip
+        self.external_ip = external_ip
+        self.status = status
+
+    def __repr__(self):
+        return (('<DimensionDataNatRule: id=%s, status=%s>')
+                % (self.id, self.status))
+
+
+class DimensionDataAntiAffinityRule(object):
+    """
+    Anti-Affinity rule for DimensionData
+
+    An Anti-Affinity rule ensures that servers in the rule will
+    not reside on the same VMware ESX host.
+    """
+    def __init__(self, id, node_list):
+        """
+        Instantiate a new :class:`DimensionDataAntiAffinityRule`
+
+        :param id: The ID of the Anti-Affinity rule
+        :type  id: ``str``
+
+        :param node_list: List of node ids that belong in this rule
+        :type  node_list: ``list`` of ``str``
+        """
+        self.id = id
+        self.node_list = node_list
+
+    def __repr__(self):
+        return (('<DimensionDataAntiAffinityRule: id=%s>')
+                % (self.id))
+
+
+class DimensionDataVlan(object):
+    """
+    DimensionData VLAN.
+    """
+
+    def __init__(self, id, name, description, location, network_domain,
+                 status, private_ipv4_range_address, private_ipv4_range_size,
+                 ipv6_range_address, ipv6_range_size, ipv4_gateway,
+                 ipv6_gateway):
+        """
+        Initialize an instance of ``DimensionDataVlan``
+
+        :param id: The ID of the VLAN
+        :type  id: ``str``
+
+        :param name: The name of the VLAN
+        :type  name: ``str``
+
+        :param description: Plan text description of the VLAN
+        :type  description: ``str``
+
+        :param location: The location (data center) of the VLAN
+        :type  location: ``NodeLocation``
+
+        :param network_domain: The Network Domain that owns this VLAN
+        :type  network_domain: :class:`DimensionDataNetworkDomain`
+
+        :param status: The status of the VLAN
+        :type  status: :class:`DimensionDataStatus`
+
+        :param private_ipv4_range_address: The host address of the VLAN
+                                            IP space
+        :type  private_ipv4_range_address: ``str``
+
+        :param private_ipv4_range_size: The size (e.g. '24') of the VLAN
+                                            as a CIDR range size
+        :type  private_ipv4_range_size: ``int``
+
+        :param ipv6_range_address: The host address of the VLAN
+                                            IP space
+        :type  ipv6_range_address: ``str``
+
+        :param ipv6_range_size: The size (e.g. '32') of the VLAN
+                                            as a CIDR range size
+        :type  ipv6_range_size: ``int``
+
+        :param ipv4_gateway: The IPv4 default gateway address
+        :type  ipv4_gateway: ``str``
+
+        :param ipv6_gateway: The IPv6 default gateway address
+        :type  ipv6_gateway: ``str``
+        """
+        self.id = str(id)
+        self.name = name
+        self.location = location
+        self.description = description
+        self.network_domain = network_domain
+        self.status = status
+        self.private_ipv4_range_address = private_ipv4_range_address
+        self.private_ipv4_range_size = private_ipv4_range_size
+        self.ipv6_range_address = ipv6_range_address
+        self.ipv6_range_size = ipv6_range_size
+        self.ipv4_gateway = ipv4_gateway
+        self.ipv6_gateway = ipv6_gateway
+
+    def __repr__(self):
+        return (('<DimensionDataVlan: id=%s, name=%s, '
+                 'description=%s, location=%s, status=%s>')
+                % (self.id, self.name, self.description,
+                   self.location, self.status))
+
+
+class DimensionDataPool(object):
+    """
+    DimensionData VIP Pool.
+    """
+
+    def __init__(self, id, name, description, status, load_balance_method,
+                 health_monitor_id, service_down_action, slow_ramp_time):
+        """
+        Initialize an instance of ``DimensionDataPool``
+
+        :param id: The ID of the pool
+        :type  id: ``str``
+
+        :param name: The name of the pool
+        :type  name: ``str``
+
+        :param description: Plan text description of the pool
+        :type  description: ``str``
+
+        :param status: The status of the pool
+        :type  status: :class:`DimensionDataStatus`
+
+        :param load_balance_method: The load balancer method
+        :type  load_balance_method: ``str``
+
+        :param health_monitor_id: The ID of the health monitor
+        :type  health_monitor_id: ``str``
+
+        :param service_down_action: Action to take when pool is down
+        :type  service_down_action: ``str``
+
+        :param slow_ramp_time: The ramp-up time for service recovery
+        :type  slow_ramp_time: ``int``
+        """
+        self.id = str(id)
+        self.name = name
+        self.description = description
+        self.status = status
+        self.load_balance_method = load_balance_method
+        self.health_monitor_id = health_monitor_id
+        self.service_down_action = service_down_action
+        self.slow_ramp_time = slow_ramp_time
+
+    def __repr__(self):
+        return (('<DimensionDataPool: id=%s, name=%s, '
+                 'description=%s, status=%s>')
+                % (self.id, self.name, self.description,
+                   self.status))
+
+
+class DimensionDataPoolMember(object):
+    """
+    DimensionData VIP Pool Member.
+    """
+
+    def __init__(self, id, name, status, ip, port, node_id):
+        """
+        Initialize an instance of ``DimensionDataPoolMember``
+
+        :param id: The ID of the pool member
+        :type  id: ``str``
+
+        :param name: The name of the pool member
+        :type  name: ``str``
+
+        :param status: The status of the pool
+        :type  status: :class:`DimensionDataStatus`
+
+        :param ip: The IP of the pool member
+        :type  ip: ``str``
+
+        :param port: The port of the pool member
+        :type  port: ``int``
+
+        :param node_id: The ID of the associated node
+        :type  node_id: ``str``
+        """
+        self.id = str(id)
+        self.name = name
+        self.status = status
+        self.ip = ip
+        self.port = port
+        self.node_id = node_id
+
+    def __repr__(self):
+        return (('<DimensionDataPoolMember: id=%s, name=%s, '
+                 'ip=%s, status=%s, port=%s, node_id=%s>')
+                % (self.id, self.name,
+                   self.ip, self.status, self.port,
+                   self.node_id))
+
+
+class DimensionDataVIPNode(object):
+    def __init__(self, id, name, status, ip, connection_limit='10000',
+                 connection_rate_limit='10000'):
+        """
+        Initialize an instance of :class:`DimensionDataVIPNode`
+
+        :param id: The ID of the node
+        :type  id: ``str``
+
+        :param name: The name of the node
+        :type  name: ``str``
+
+        :param status: The status of the node
+        :type  status: :class:`DimensionDataStatus`
+
+        :param ip: The IP of the node
+        :type  ip: ``str``
+
+        :param connection_limit: The total connection limit for the node
+        :type  connection_limit: ``int``
+
+        :param connection_rate_limit: The rate limit for the node
+        :type  connection_rate_limit: ``int``
+        """
+        self.id = str(id)
+        self.name = name
+        self.status = status
+        self.ip = ip
+        self.connection_limit = connection_limit
+        self.connection_rate_limit = connection_rate_limit
+
+    def __repr__(self):
+        return (('<DimensionDataVIPNode: id=%s, name=%s, '
+                 'status=%s, ip=%s>')
+                % (self.id, self.name,
+                   self.status, self.ip))
+
+
+class DimensionDataVirtualListener(object):
+    """
+    DimensionData Virtual Listener.
+    """
+
+    def __init__(self, id, name, status, ip):
+        """
+        Initialize an instance of :class:`DimensionDataVirtualListener`
+
+        :param id: The ID of the listener
+        :type  id: ``str``
+
+        :param name: The name of the listener
+        :type  name: ``str``
+
+        :param status: The status of the listener
+        :type  status: :class:`DimensionDataStatus`
+
+        :param ip: The IP of the listener
+        :type  ip: ``str``
+        """
+        self.id = str(id)
+        self.name = name
+        self.status = status
+        self.ip = ip
+
+    def __repr__(self):
+        return (('<DimensionDataVirtualListener: id=%s, name=%s, '
+                 'status=%s, ip=%s>')
+                % (self.id, self.name,
+                   self.status, self.ip))
+
+
+class DimensionDataDefaultHealthMonitor(object):
+    """
+    A default health monitor for a VIP (node, pool or listener)
+    """
+    def __init__(self, id, name, node_compatible, pool_compatible):
+        """
+        Initialize an instance of :class:`DimensionDataDefaultHealthMonitor`
+
+        :param id: The ID of the monitor
+        :type  id: ``str``
+
+        :param name: The name of the monitor
+        :type  name: ``str``
+
+        :param node_compatible: Is a monitor capable of monitoring nodes
+        :type  node_compatible: ``bool``
+
+        :param pool_compatible: Is a monitor capable of monitoring pools
+        :type  pool_compatible: ``bool``
+        """
+        self.id = id
+        self.name = name
+        self.node_compatible = node_compatible
+        self.pool_compatible = pool_compatible
+
+    def __repr__(self):
+        return (('<DimensionDataDefaultHealthMonitor: id=%s, name=%s>')
+                % (self.id, self.name))
+
+
+class DimensionDataPersistenceProfile(object):
+    """
+    Each Persistence Profile declares the combination of Virtual Listener
+    type and protocol with which it is
+    compatible and whether or not it is compatible as a
+    Fallback Persistence Profile.
+    """
+    def __init__(self, id, name, compatible_listeners, fallback_compatible):
+        """
+        Initialize an instance of :class:`DimensionDataPersistenceProfile`
+
+        :param id: The ID of the profile
+        :type  id: ``str``
+
+        :param name: The name of the profile
+        :type  name: ``str``
+
+        :param compatible_listeners: List of compatible Virtual Listener types
+        :type  compatible_listeners: ``list`` of
+            :class:`DimensionDataVirtualListenerCompatibility`
+
+        :param fallback_compatible: Is capable as a fallback profile
+        :type  fallback_compatible: ``bool``
+        """
+        self.id = id
+        self.name = name
+        self.compatible_listeners = compatible_listeners
+        self.fallback_compatible = fallback_compatible
+
+    def __repr__(self):
+        return (('<DimensionDataPersistenceProfile: id=%s, name=%s>')
+                % (self.id, self.name))
+
+
+class DimensionDataDefaultiRule(object):
+    """
+    A default iRule for a network domain, can be applied to a listener
+    """
+    def __init__(self, id, name, compatible_listeners):
+        """
+        Initialize an instance of :class:`DimensionDataDefaultiRule`
+
+        :param id: The ID of the iRule
+        :type  id: ``str``
+
+        :param name: The name of the iRule
+        :type  name: ``str``
+
+        :param compatible_listeners: List of compatible Virtual Listener types
+        :type  compatible_listeners: ``list`` of
+            :class:`DimensionDataVirtualListenerCompatibility`
+        """
+        self.id = id
+        self.name = name
+        self.compatible_listeners = compatible_listeners
+
+    def __repr__(self):
+        return (('<DimensionDataDefaultiRule: id=%s, name=%s>')
+                % (self.id, self.name))
+
+
+class DimensionDataVirtualListenerCompatibility(object):
+    """
+    A compatibility preference for a persistence profile or iRule
+    specifies which virtual listener types this profile or iRule can be
+    applied to.
+    """
+    def __init__(self, type, protocol):
+        self.type = type
+        self.protocol = protocol
+
+    def __repr__(self):
+        return (('<DimensionDataVirtualListenerCompatibility: '
+                 'type=%s, protocol=%s>')
+                % (self.type, self.protocol))
+
+
+class DimensionDataBackupDetails(object):
+    """
+    Dimension Data Backup Details represents information about
+    a targets backups configuration
+    """
+
+    def __init__(self, asset_id, service_plan, status, clients=None):
+        """
+        Initialize an instance of :class:`DimensionDataBackupDetails`
+
+        :param asset_id: Asset identification for backups
+        :type  asset_id: ``str``
+
+        :param service_plan: The service plan for backups. i.e (Essentials)
+        :type  service_plan: ``str``
+
+        :param status: The overall status this backup target.
+                       i.e. (unregistered)
+        :type  status: ``str``
+
+        :param clients: Backup clients attached to this target
+        :type  clients: ``list`` of :class:`DimensionDataBackupClient`
+        """
+        self.asset_id = asset_id
+        self.service_plan = service_plan
+        self.status = status
+        self.clients = clients
+
+    def __repr__(self):
+        return (('<DimensionDataBackupDetails: id=%s>')
+                % (self.asset_id))
+
+
+class DimensionDataBackupClient(object):
+    """
+    An object that represents a backup client
+    """
+    def __init__(self, id, type, status,
+                 schedule_policy, storage_policy, download_url,
+                 alert=None, running_job=None):
+        """
+        Initialize an instance of :class:`DimensionDataBackupClient`
+
+        :param id: Unique ID for the client
+        :type  id: ``str``
+
+        :param type: The type of client that this client is
+        :type  type: :class:`DimensionDataBackupClientType`
+
+        :param status: The states of this particular backup client.
+                       i.e. (Unregistered)
+        :type  status: ``str``
+
+        :param schedule_policy: The schedule policy for this client
+                                NOTE: Dimension Data only sends back the name
+                                of the schedule policy, no further details
+        :type  schedule_policy: ``str``
+
+        :param storage_policy: The storage policy for this client
+                               NOTE: Dimension Data only sends back the name
+                               of the storage policy, no further details
+        :type  storage_policy: ``str``
+
+        :param download_url: The download url for this client
+        :type  download_url: ``str``
+
+        :param alert: The alert configured for this backup client (optional)
+        :type  alert: :class:`DimensionDataBackupClientAlert`
+
+        :param alert: The running job for the client (optional)
+        :type  alert: :class:`DimensionDataBackupClientRunningJob`
+        """
+        self.id = id
+        self.type = type
+        self.status = status
+        self.schedule_policy = schedule_policy
+        self.storage_policy = storage_policy
+        self.download_url = download_url
+        self.alert = alert
+        self.running_job = running_job
+
+    def __repr__(self):
+        return (('<DimensionDataBackupClient: id=%s>')
+                % (self.id))
+
+
+class DimensionDataBackupClientAlert(object):
+    """
+    An alert for a backup client
+    """
+    def __init__(self, trigger, notify_list=[]):
+        """
+        Initialize an instance of :class:`DimensionDataBackupClientAlert`
+
+        :param trigger: Trigger type for the client i.e. ON_FAILURE
+        :type  trigger: ``str``
+
+        :param notify_list: List of email addresses that are notified
+                            when the alert is fired
+        :type  notify_list: ``list`` of ``str``
+        """
+        self.trigger = trigger
+        self.notify_list = notify_list
+
+    def __repr__(self):
+        return (('<DimensionDataBackupClientAlert: trigger=%s>')
+                % (self.trigger))
+
+
+class DimensionDataBackupClientRunningJob(object):
+    """
+    A running job for a given backup client
+    """
+    def __init__(self, id, status, percentage=0):
+        """
+        Initialize an instance of :class:`DimensionDataBackupClientRunningJob`
+
+        :param id: The unqiue ID of the job
+        :type  id: ``str``
+
+        :param status: The status of the job i.e. Waiting
+        :type  status: ``str``
+
+        :param percentage: The percentage completion of the job
+        :type  percentage: ``int``
+        """
+        self.id = id
+        self.percentage = percentage
+        self.status = status
+
+    def __repr__(self):
+        return (('<DimensionDataBackupClientRunningJob: id=%s>')
+                % (self.id))
+
+
+class DimensionDataBackupClientType(object):
+    """
+    A client type object for backups
+    """
+    def __init__(self, type, is_file_system, description):
+        """
+        Initialize an instance of :class:`DimensionDataBackupClientType`
+
+        :param type: The type of client i.e. (FA.Linux, MySQL, ect.)
+        :type  type: ``str``
+
+        :param is_file_system: The name of the iRule
+        :type  is_file_system: ``bool``
+
+        :param description: Description of the client
+        :type  description: ``str``
+        """
+        self.type = type
+        self.is_file_system = is_file_system
+        self.description = description
+
+    def __repr__(self):
+        return (('<DimensionDataBackupClientType: type=%s>')
+                % (self.type))
+
+
+class DimensionDataBackupStoragePolicy(object):
+    """
+    A representation of a storage policy
+    """
+    def __init__(self, name, retention_period, secondary_location):
+        """
+        Initialize an instance of :class:`DimensionDataBackupStoragePolicy`
+
+        :param name: The name of the storage policy i.e. 14 Day Storage Policy
+        :type  name: ``str``
+
+        :param retention_period: How long to keep the backup in days
+        :type  retention_period: ``int``
+
+        :param secondary_location: The secondary location i.e. Primary
+        :type  secondary_location: ``str``
+        """
+        self.name = name
+        self.retention_period = retention_period
+        self.secondary_location = secondary_location
+
+    def __repr__(self):
+        return (('<DimensionDataBackupStoragePolicy: name=%s>')
+                % (self.name))
+
+
+class DimensionDataBackupSchedulePolicy(object):
+    """
+    A representation of a schedule policy
+    """
+    def __init__(self, name, description):
+        """
+        Initialize an instance of :class:`DimensionDataBackupSchedulePolicy`
+
+        :param name: The name of the policy i.e 12AM - 6AM
+        :type  name: ``str``
+
+        :param description: Short summary of the details of the policy
+        :type  description: ``str``
+        """
+        self.name = name
+        self.description = description
+
+    def __repr__(self):
+        return (('<DimensionDataBackupSchedulePolicy: name=%s>')
+                % (self.name))
+
+
+class DimensionDataTag(object):
+    """
+    A representation of a Tag in Dimension Data
+    A Tag first must have a Tag Key, then an asset is tag with
+    a key and an option value.  Tags can be queried later to filter assets
+    and also show up on usage report if so desired.
+    """
+    def __init__(self, asset_type, asset_id, asset_name,
+                 datacenter, key, value):
+        """
+        Initialize an instance of :class:`DimensionDataTag`
+
+        :param asset_type: The type of asset.  Current asset types:
+                           SERVER, VLAN, NETWORK_DOMAIN, CUSTOMER_IMAGE,
+                           PUBLIC_IP_BLOCK, ACCOUNT
+        :type  asset_type: ``str``
+
+        :param asset_id: The GUID of the asset that is tagged
+        :type  asset_id: ``str``
+
+        :param asset_name: The name of the asset that is tagged
+        :type  asset_name: ``str``
+
+        :param datacenter: The short datacenter name of the tagged asset
+        :type  datacenter: ``str``
+
+        :param key: The tagged key
+        :type  key: :class:`DimensionDataTagKey`
+
+        :param value: The tagged value
+        :type  value: ``None`` or ``str``
+        """
+        self.asset_type = asset_type
+        self.asset_id = asset_id
+        self.asset_name = asset_name
+        self.datacenter = datacenter
+        self.key = key
+        self.value = value
+
+    def __repr__(self):
+        return (('<DimensionDataTag: asset_name=%s, tag_name=%s, value=%s>')
+                % (self.asset_name, self.key.name, self.value))
+
+
+class DimensionDataTagKey(object):
+    """
+    A representation of a Tag Key in Dimension Data
+    A tag key is required to tag an asset
+    """
+    def __init__(self, id, name, description,
+                 value_required, display_on_report):
+        """
+        Initialize an instance of :class:`DimensionDataTagKey`
+
+        :param id: GUID of the tag key
+        :type  id: ``str``
+
+        :param name: Name of the tag key
+        :type  name: ``str``
+
+        :param description: Description of the tag key
+        :type  description: ``str``
+
+        :param value_required: If a value is required for this tag key
+        :type  value_required: ``bool``
+
+        :param display_on_report: If this tag key should be displayed on
+                                  usage reports
+        :type  display_on_report: ``bool``
+        """
+        self.id = id
+        self.name = name
+        self.description = description
+        self.value_required = value_required
+        self.display_on_report = display_on_report
+
+    def __repr__(self):
+        return (('<DimensionDataTagKey: name=%s>')
+                % (self.name))
diff --git a/libcloud/compute/drivers/dimensiondata.py b/libcloud/compute/drivers/dimensiondata.py
--- a/libcloud/compute/drivers/dimensiondata.py
+++ b/libcloud/compute/drivers/dimensiondata.py
@@ -44,7 +44,7 @@
 from libcloud.common.dimensiondata import API_ENDPOINTS, DEFAULT_REGION
 from libcloud.common.dimensiondata import TYPES_URN
 from libcloud.common.dimensiondata import SERVER_NS, NETWORK_NS, GENERAL_NS
-from libcloud.utils.py3 import urlencode
+from libcloud.utils.py3 import urlencode, ensure_string
 from libcloud.utils.xml import fixxpath, findtext, findall
 from libcloud.utils.py3 import basestring
 from libcloud.compute.types import NodeState, Provider
@@ -2289,6 +2289,101 @@ def ex_list_tags(self, asset_id=None, asset_type=None, location=None,
             tags.extend(self._to_tags(result))
         return tags
 
+    def ex_summary_usage_report(self, start_date, end_date):
+        """
+        Get summary usage information
+
+        :param start_date: Start date for the report
+        :type  start_date: ``str`` in format YYYY-MM-DD
+
+        :param end_date: End date for the report
+        :type  end_date: ``str`` in format YYYY-MM-DD
+
+        :rtype: ``list`` of ``list``
+        """
+        result = self.connection.raw_request_with_orgId_api_1(
+            'report/usage?startDate=%s&endDate=%s' % (
+                start_date, end_date))
+        return self._format_csv(result.response)
+
+    def ex_detailed_usage_report(self, start_date, end_date):
+        """
+        Get detailed usage information
+
+        :param start_date: Start date for the report
+        :type  start_date: ``str`` in format YYYY-MM-DD
+
+        :param end_date: End date for the report
+        :type  end_date: ``str`` in format YYYY-MM-DD
+
+        :rtype: ``list`` of ``list``
+        """
+        result = self.connection.raw_request_with_orgId_api_1(
+            'report/usageDetailed?startDate=%s&endDate=%s' % (
+                start_date, end_date))
+        return self._format_csv(result.response)
+
+    def ex_software_usage_report(self, start_date, end_date):
+        """
+        Get detailed software usage reports
+
+        :param start_date: Start date for the report
+        :type  start_date: ``str`` in format YYYY-MM-DD
+
+        :param end_date: End date for the report
+        :type  end_date: ``str`` in format YYYY-MM-DD
+
+        :rtype: ``list`` of ``list``
+        """
+        result = self.connection.raw_request_with_orgId_api_1(
+            'report/usageSoftwareUnits?startDate=%s&endDate=%s' % (
+                start_date, end_date))
+        return self._format_csv(result.response)
+
+    def ex_audit_log_report(self, start_date, end_date):
+        """
+        Get audit log report
+
+        :param start_date: Start date for the report
+        :type  start_date: ``str`` in format YYYY-MM-DD
+
+        :param end_date: End date for the report
+        :type  end_date: ``str`` in format YYYY-MM-DD
+
+        :rtype: ``list`` of ``list``
+        """
+        result = self.connection.raw_request_with_orgId_api_1(
+            'report/usageSoftwareUnits?startDate=%s&endDate=%s' % (
+                start_date, end_date))
+        return self._format_csv(result.response)
+
+    def ex_backup_usage_report(self, start_date, end_date, location):
+        """
+        Get audit log report
+
+        :param start_date: Start date for the report
+        :type  start_date: ``str`` in format YYYY-MM-DD
+
+        :param end_date: End date for the report
+        :type  end_date: ``str`` in format YYYY-MM-DD
+
+        :keyword location: Filters the node list to nodes that are
+                           located in this location
+        :type    location: :class:`NodeLocation` or ``str``
+
+        :rtype: ``list`` of ``list``
+        """
+        datacenter_id = self._location_to_location_id(location)
+        result = self.connection.raw_request_with_orgId_api_1(
+            'backup/detailedUsageReport?datacenterId=%s&fromDate=%s&toDate=%s'
+            % (datacenter_id, start_date, end_date))
+        return self._format_csv(result.response)
+
+    def _format_csv(self, http_response):
+        text = http_response.read()
+        lines = str.splitlines(ensure_string(text))
+        return [line.split(',') for line in lines]
+
     @staticmethod
     def _get_tagging_asset_type(asset):
         objecttype = type(asset)


Test patch:
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_DISABLE.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_DISABLE.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_DISABLE.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_DISABLE.xml
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns13:Status xmlns:ns16="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns="http://oec.api.opsource.net/schemas/server" xmlns:ns14="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns15="http://oec.api.opsource.net/schemas/reset" xmlns:ns9="http://oec.api.opsource.net/schemas/storage" xmlns:ns5="http://oec.api.opsource.net/schemas/backup" xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns13="http://oec.api.opsource.net/schemas/general" xmlns:ns6="http://oec.api.opsource.net/schemas/support" xmlns:ns7="http://oec.api.opsource.net/schemas/organization" xmlns:ns10="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns8="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns11="http://oec.api.opsource.net/schemas/vip" xmlns:ns2="http://oec.api.opsource.net/schemas/network" xmlns:ns4="http://oec.api.opsource.net/schemas/directory" xmlns:ns3="http://oec.api.opsource.net/schemas/admin">
-    <ns13:operation>Disable Backup for Server</ns13:operation>
-    <ns13:result>SUCCESS</ns13:result>
-    <ns13:resultDetail>Backup disabled for Server</ns13:resultDetail>
-    <ns13:resultCode>REASON_0</ns13:resultCode>
-</ns13:Status>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns13:Status xmlns:ns16="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns="http://oec.api.opsource.net/schemas/server" xmlns:ns14="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns15="http://oec.api.opsource.net/schemas/reset" xmlns:ns9="http://oec.api.opsource.net/schemas/storage" xmlns:ns5="http://oec.api.opsource.net/schemas/backup" xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns13="http://oec.api.opsource.net/schemas/general" xmlns:ns6="http://oec.api.opsource.net/schemas/support" xmlns:ns7="http://oec.api.opsource.net/schemas/organization" xmlns:ns10="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns8="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns11="http://oec.api.opsource.net/schemas/vip" xmlns:ns2="http://oec.api.opsource.net/schemas/network" xmlns:ns4="http://oec.api.opsource.net/schemas/directory" xmlns:ns3="http://oec.api.opsource.net/schemas/admin">
+    <ns13:operation>Disable Backup for Server</ns13:operation>
+    <ns13:result>SUCCESS</ns13:result>
+    <ns13:resultDetail>Backup disabled for Server</ns13:resultDetail>
+    <ns13:resultCode>REASON_0</ns13:resultCode>
+</ns13:Status>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_ENABLE.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_ENABLE.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_ENABLE.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_ENABLE.xml
@@ -1,18 +1,18 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage"
-            xmlns="http://oec.api.opsource.net/schemas/server"
-            xmlns:ns14="http://oec.api.opsource.net/schemas/support"
-            xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo"
-            xmlns:ns9="http://oec.api.opsource.net/schemas/admin"
-            xmlns:ns5="http://oec.api.opsource.net/schemas/vip"
-            xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel"
-            xmlns:ns13="http://oec.api.opsource.net/schemas/reset"
-            xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns10="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns8="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns11="http://oec.api.opsource.net/schemas/backup" xmlns:ns2="http://oec.api.opsource.net/schemas/directory" xmlns:ns4="http://oec.api.opsource.net/schemas/network" xmlns:ns3="http://oec.api.opsource.net/schemas/organization">
-    <ns6:operation>Enable Backup for Server</ns6:operation>
-    <ns6:result>SUCCESS</ns6:result>
-    <ns6:resultDetail>Backup enabled for Server - see additional Information for Asset ID</ns6:resultDetail>
-    <ns6:resultCode>REASON_0</ns6:resultCode>
-    <ns6:additionalInformation name="assetId">
-        <ns6:value>ee7c4b64-f7af-4a4f-8384-be362273530f</ns6:value>
-    </ns6:additionalInformation>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage"
+            xmlns="http://oec.api.opsource.net/schemas/server"
+            xmlns:ns14="http://oec.api.opsource.net/schemas/support"
+            xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo"
+            xmlns:ns9="http://oec.api.opsource.net/schemas/admin"
+            xmlns:ns5="http://oec.api.opsource.net/schemas/vip"
+            xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel"
+            xmlns:ns13="http://oec.api.opsource.net/schemas/reset"
+            xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns10="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns8="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns11="http://oec.api.opsource.net/schemas/backup" xmlns:ns2="http://oec.api.opsource.net/schemas/directory" xmlns:ns4="http://oec.api.opsource.net/schemas/network" xmlns:ns3="http://oec.api.opsource.net/schemas/organization">
+    <ns6:operation>Enable Backup for Server</ns6:operation>
+    <ns6:result>SUCCESS</ns6:result>
+    <ns6:resultDetail>Backup enabled for Server - see additional Information for Asset ID</ns6:resultDetail>
+    <ns6:resultCode>REASON_0</ns6:resultCode>
+    <ns6:additionalInformation name="assetId">
+        <ns6:value>ee7c4b64-f7af-4a4f-8384-be362273530f</ns6:value>
+    </ns6:additionalInformation>
 </ns6:Status>
\ No newline at end of file
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_EXISTS.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_EXISTS.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_EXISTS.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_EXISTS.xml
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns0:Status xmlns:ns0="http://oec.api.opsource.net/schemas/general">
-    <ns0:operation>Enable Backup for Server</ns0:operation>
-    <ns0:result>ERROR</ns0:result>
-    <ns0:resultDetail>Cloud backup for this server is already enabled or being enabled (state: NORMAL).</ns0:resultDetail>
-    <ns0:resultCode>REASON_550</ns0:resultCode>
-</ns0:Status>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns0:Status xmlns:ns0="http://oec.api.opsource.net/schemas/general">
+    <ns0:operation>Enable Backup for Server</ns0:operation>
+    <ns0:result>ERROR</ns0:result>
+    <ns0:resultDetail>Cloud backup for this server is already enabled or being enabled (state: NORMAL).</ns0:resultDetail>
+    <ns0:resultCode>REASON_550</ns0:resultCode>
+</ns0:Status>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO.xml
@@ -1,16 +1,16 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns9:BackupDetails assetId="12769311-938c-4669-9460-7723eb194269" servicePlan="Enterprise" state="NORMAL" xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
-    <ns9:backupClient id="30b1ff76-c76d-4d7c-b39d-3b72be0384c8" type="FA.Linux" isFileSystem="true" status="Unregistered">
-        <ns9:description>Linux File Agent</ns9:description>
-        <ns9:schedulePolicyName>12AM - 6AM</ns9:schedulePolicyName>
-        <ns9:storagePolicyName>14 Day Storage Policy</ns9:storagePolicyName>
-        <ns9:alerting trigger="ON_FAILURE">
-            <ns9:emailAddress>fake_email@example.com</ns9:emailAddress>
-            <ns9:emailAddress>fake_email2@example.com</ns9:emailAddress>
-        </ns9:alerting>
-        <ns9:times nextBackup="2016-02-09T00:00:00" lastOnline="2016-02-08T06:10:25"/>
-        <ns9:totalBackupSizeGb>0</ns9:totalBackupSizeGb>
-        <ns9:downloadUrl>https://backups-na.cloud-vpn.net/PCS/BackupClientInstallerDownload/cbb8a8c607ca4144e8828814edfc1634c8dd8782</ns9:downloadUrl>
-        <ns9:runningJob id="106130" name="Backup" percentageComplete="5" status="Waiting"/>
-    </ns9:backupClient>
-</ns9:BackupDetails>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns9:BackupDetails assetId="12769311-938c-4669-9460-7723eb194269" servicePlan="Enterprise" state="NORMAL" xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
+    <ns9:backupClient id="30b1ff76-c76d-4d7c-b39d-3b72be0384c8" type="FA.Linux" isFileSystem="true" status="Unregistered">
+        <ns9:description>Linux File Agent</ns9:description>
+        <ns9:schedulePolicyName>12AM - 6AM</ns9:schedulePolicyName>
+        <ns9:storagePolicyName>14 Day Storage Policy</ns9:storagePolicyName>
+        <ns9:alerting trigger="ON_FAILURE">
+            <ns9:emailAddress>fake_email@example.com</ns9:emailAddress>
+            <ns9:emailAddress>fake_email2@example.com</ns9:emailAddress>
+        </ns9:alerting>
+        <ns9:times nextBackup="2016-02-09T00:00:00" lastOnline="2016-02-08T06:10:25"/>
+        <ns9:totalBackupSizeGb>0</ns9:totalBackupSizeGb>
+        <ns9:downloadUrl>https://backups-na.cloud-vpn.net/PCS/BackupClientInstallerDownload/cbb8a8c607ca4144e8828814edfc1634c8dd8782</ns9:downloadUrl>
+        <ns9:runningJob id="106130" name="Backup" percentageComplete="5" status="Waiting"/>
+    </ns9:backupClient>
+</ns9:BackupDetails>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_DISABLED.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_DISABLED.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_DISABLED.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_DISABLED.xml
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
-    <ns6:operation>Get Backup Details</ns6:operation>
-    <ns6:result>ERROR</ns6:result>
-    <ns6:resultDetail>Server e75ead52-692f-4314-8725-c8a4f4d13a87 has not been provisioned for backup</ns6:resultDetail>
-    <ns6:resultCode>REASON_543</ns6:resultCode>
-</ns6:Status>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
+    <ns6:operation>Get Backup Details</ns6:operation>
+    <ns6:result>ERROR</ns6:result>
+    <ns6:resultDetail>Server e75ead52-692f-4314-8725-c8a4f4d13a87 has not been provisioned for backup</ns6:resultDetail>
+    <ns6:resultCode>REASON_543</ns6:resultCode>
+</ns6:Status>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_NOCLIENT.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_NOCLIENT.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_NOCLIENT.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_NOCLIENT.xml
@@ -1,2 +1,2 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns9:BackupDetails assetId="71febbbc-337a-40c4-8c29-0afe85ccb3ea" servicePlan="Essentials" state="NORMAL" xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip"/>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns9:BackupDetails assetId="71febbbc-337a-40c4-8c29-0afe85ccb3ea" servicePlan="Essentials" state="NORMAL" xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip"/>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_NOJOB.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_NOJOB.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_NOJOB.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_INFO_NOJOB.xml
@@ -1,11 +1,11 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns9:BackupDetails assetId="12769311-938c-4669-9460-7723eb194269" servicePlan="Enterprise" state="NORMAL" xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
-    <ns9:backupClient id="30b1ff76-c76d-4d7c-b39d-3b72be0384c8" type="FA.Linux" isFileSystem="true" status="Unregistered">
-        <ns9:description>Linux File Agent</ns9:description>
-        <ns9:schedulePolicyName>12AM - 6AM</ns9:schedulePolicyName>
-        <ns9:storagePolicyName>14 Day Storage Policy</ns9:storagePolicyName>
-        <ns9:times nextBackup="2016-02-09T00:00:00" lastOnline="2016-02-08T06:10:25"/>
-        <ns9:totalBackupSizeGb>0</ns9:totalBackupSizeGb>
-        <ns9:downloadUrl>https://backups-na.cloud-vpn.net/PCS/BackupClientInstallerDownload/cbb8a8c607ca4144e8828814edfc1634c8dd8782</ns9:downloadUrl>
-    </ns9:backupClient>
-</ns9:BackupDetails>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns9:BackupDetails assetId="12769311-938c-4669-9460-7723eb194269" servicePlan="Enterprise" state="NORMAL" xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
+    <ns9:backupClient id="30b1ff76-c76d-4d7c-b39d-3b72be0384c8" type="FA.Linux" isFileSystem="true" status="Unregistered">
+        <ns9:description>Linux File Agent</ns9:description>
+        <ns9:schedulePolicyName>12AM - 6AM</ns9:schedulePolicyName>
+        <ns9:storagePolicyName>14 Day Storage Policy</ns9:storagePolicyName>
+        <ns9:times nextBackup="2016-02-09T00:00:00" lastOnline="2016-02-08T06:10:25"/>
+        <ns9:totalBackupSizeGb>0</ns9:totalBackupSizeGb>
+        <ns9:downloadUrl>https://backups-na.cloud-vpn.net/PCS/BackupClientInstallerDownload/cbb8a8c607ca4144e8828814edfc1634c8dd8782</ns9:downloadUrl>
+    </ns9:backupClient>
+</ns9:BackupDetails>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob.xml
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
-    <ns6:operation>Cancel Backup Job</ns6:operation>
-    <ns6:result>SUCCESS</ns6:result>
-    <ns6:resultDetail>Backup Job Canceled</ns6:resultDetail>
-    <ns6:resultCode>REASON_0</ns6:resultCode>
-</ns6:Status>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
+    <ns6:operation>Cancel Backup Job</ns6:operation>
+    <ns6:result>SUCCESS</ns6:result>
+    <ns6:resultDetail>Backup Job Canceled</ns6:resultDetail>
+    <ns6:resultCode>REASON_0</ns6:resultCode>
+</ns6:Status>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob_FAIL.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob_FAIL.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob_FAIL.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob_FAIL.xml
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
-    <ns6:operation>Cancel Backup Job</ns6:operation>
-    <ns6:result>ERROR</ns6:result>
-    <ns6:resultDetail>No backup job currently running on client</ns6:resultDetail>
-    <ns6:resultCode>REASON_547</ns6:resultCode>
-</ns6:Status>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/admin" xmlns:ns14="http://oec.api.opsource.net/schemas/directory" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/backup" xmlns:ns5="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns12="http://oec.api.opsource.net/schemas/support" xmlns:ns13="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/reset" xmlns:ns10="http://oec.api.opsource.net/schemas/server" xmlns:ns8="http://oec.api.opsource.net/schemas/network" xmlns:ns11="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns2="http://oec.api.opsource.net/schemas/organization" xmlns:ns4="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns3="http://oec.api.opsource.net/schemas/vip">
+    <ns6:operation>Cancel Backup Job</ns6:operation>
+    <ns6:result>ERROR</ns6:result>
+    <ns6:resultDetail>No backup job currently running on client</ns6:resultDetail>
+    <ns6:resultCode>REASON_547</ns6:resultCode>
+</ns6:Status>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_SUCCESS_PUT.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_SUCCESS_PUT.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_SUCCESS_PUT.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_SUCCESS_PUT.xml
@@ -1,13 +1,13 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns13:Status xmlns:ns16="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns="http://oec.api.opsource.net/schemas/server" xmlns:ns14="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns15="http://oec.api.opsource.net/schemas/reset" xmlns:ns9="http://oec.api.opsource.net/schemas/storage" xmlns:ns5="http://oec.api.opsource.net/schemas/backup" xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns13="http://oec.api.opsource.net/schemas/general" xmlns:ns6="http://oec.api.opsource.net/schemas/support" xmlns:ns7="http://oec.api.opsource.net/schemas/organization" xmlns:ns10="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns8="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns11="http://oec.api.opsource.net/schemas/vip" xmlns:ns2="http://oec.api.opsource.net/schemas/network" xmlns:ns4="http://oec.api.opsource.net/schemas/directory" xmlns:ns3="http://oec.api.opsource.net/schemas/admin">
-    <ns13:operation>Add Backup Client</ns13:operation>
-    <ns13:result>SUCCESS</ns13:result>
-    <ns13:resultDetail>Backup Client added</ns13:resultDetail>
-    <ns13:resultCode>REASON_0</ns13:resultCode>
-    <ns13:additionalInformation name="backupClient.id">
-        <ns13:value>a26cead0-6bab-4446-9a16-c227e6ab201f</ns13:value>
-    </ns13:additionalInformation>
-    <ns13:additionalInformation name="backupClient.downloadUrl">
-        <ns13:value>https://backups-na.cloud-vpn.net/PCS/BackupClientInstallerDownload/4b3f35815b848ce7270186b52e5817fa5de0fe42</ns13:value>
-    </ns13:additionalInformation>
-</ns13:Status>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns13:Status xmlns:ns16="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns="http://oec.api.opsource.net/schemas/server" xmlns:ns14="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns15="http://oec.api.opsource.net/schemas/reset" xmlns:ns9="http://oec.api.opsource.net/schemas/storage" xmlns:ns5="http://oec.api.opsource.net/schemas/backup" xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns13="http://oec.api.opsource.net/schemas/general" xmlns:ns6="http://oec.api.opsource.net/schemas/support" xmlns:ns7="http://oec.api.opsource.net/schemas/organization" xmlns:ns10="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns8="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns11="http://oec.api.opsource.net/schemas/vip" xmlns:ns2="http://oec.api.opsource.net/schemas/network" xmlns:ns4="http://oec.api.opsource.net/schemas/directory" xmlns:ns3="http://oec.api.opsource.net/schemas/admin">
+    <ns13:operation>Add Backup Client</ns13:operation>
+    <ns13:result>SUCCESS</ns13:result>
+    <ns13:resultDetail>Backup Client added</ns13:resultDetail>
+    <ns13:resultCode>REASON_0</ns13:resultCode>
+    <ns13:additionalInformation name="backupClient.id">
+        <ns13:value>a26cead0-6bab-4446-9a16-c227e6ab201f</ns13:value>
+    </ns13:additionalInformation>
+    <ns13:additionalInformation name="backupClient.downloadUrl">
+        <ns13:value>https://backups-na.cloud-vpn.net/PCS/BackupClientInstallerDownload/4b3f35815b848ce7270186b52e5817fa5de0fe42</ns13:value>
+    </ns13:additionalInformation>
+</ns13:Status>
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_schedulePolicy.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_schedulePolicy.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_schedulePolicy.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_schedulePolicy.xml
@@ -1,5 +1,5 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<BackupSchedulePolicies xmlns="http://oec.api.opsource.net/schemas/backup">
- <!--One or more repetitions, all attributes mandatory -->
- <schedulePolicy name="12AM - 6AM" description="Daily backup will start between 12AM - 6AM"/>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<BackupSchedulePolicies xmlns="http://oec.api.opsource.net/schemas/backup">
+ <!--One or more repetitions, all attributes mandatory -->
+ <schedulePolicy name="12AM - 6AM" description="Daily backup will start between 12AM - 6AM"/>
 </BackupSchedulePolicies>
\ No newline at end of file
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_storagePolicy.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_storagePolicy.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_storagePolicy.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_storagePolicy.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<BackupStoragePolicies xmlns="http://oec.api.opsource.net/schemas/backup">
- <!-- One or more repetitions. Attribute secondaryLocation is optional, all
-others mandatory -->
- <storagePolicy retentionPeriodInDays="30" name="30 Day Storage Policy + Secondary Copy" secondaryLocation="Primary"/>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<BackupStoragePolicies xmlns="http://oec.api.opsource.net/schemas/backup">
+ <!-- One or more repetitions. Attribute secondaryLocation is optional, all
+others mandatory -->
+ <storagePolicy retentionPeriodInDays="30" name="30 Day Storage Policy + Secondary Copy" secondaryLocation="Primary"/>
 </BackupStoragePolicies>
\ No newline at end of file
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_type.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_type.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_client_type.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_client_type.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<BackupClientTypes xmlns="http://oec.api.opsource.net/schemas/backup">
- <!--Zero or more repetitions, all attributes mandatory -->
- <backupClientType type="FA.Linux" isFileSystem="true" description="Linux
-File system"/>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<BackupClientTypes xmlns="http://oec.api.opsource.net/schemas/backup">
+ <!--Zero or more repetitions, all attributes mandatory -->
+ <backupClientType type="FA.Linux" isFileSystem="true" description="Linux
+File system"/>
 </BackupClientTypes>
\ No newline at end of file
diff --git a/libcloud/test/backup/fixtures/dimensiondata/_backup_modify.xml b/libcloud/test/backup/fixtures/dimensiondata/_backup_modify.xml
--- a/libcloud/test/backup/fixtures/dimensiondata/_backup_modify.xml
+++ b/libcloud/test/backup/fixtures/dimensiondata/_backup_modify.xml
@@ -1,7 +1,7 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/server" xmlns:ns14="http://oec.api.opsource.net/schemas/support" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/admin" xmlns:ns5="http://oec.api.opsource.net/schemas/vip" xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns13="http://oec.api.opsource.net/schemas/reset" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns10="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns8="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns11="http://oec.api.opsource.net/schemas/backup" xmlns:ns2="http://oec.api.opsource.net/schemas/directory" xmlns:ns4="http://oec.api.opsource.net/schemas/network" xmlns:ns3="http://oec.api.opsource.net/schemas/organization">
-    <ns6:operation>Change Backup Service Plan</ns6:operation>
-    <ns6:result>SUCCESS</ns6:result>
-    <ns6:resultDetail>Backup Service Plan changed</ns6:resultDetail>
-    <ns6:resultCode>REASON_0</ns6:resultCode>
-</ns6:Status>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<ns6:Status xmlns:ns16="http://oec.api.opsource.net/schemas/storage" xmlns="http://oec.api.opsource.net/schemas/server" xmlns:ns14="http://oec.api.opsource.net/schemas/support" xmlns:ns15="http://oec.api.opsource.net/schemas/multigeo" xmlns:ns9="http://oec.api.opsource.net/schemas/admin" xmlns:ns5="http://oec.api.opsource.net/schemas/vip" xmlns:ns12="http://oec.api.opsource.net/schemas/whitelabel" xmlns:ns13="http://oec.api.opsource.net/schemas/reset" xmlns:ns6="http://oec.api.opsource.net/schemas/general" xmlns:ns7="http://oec.api.opsource.net/schemas/datacenter" xmlns:ns10="http://oec.api.opsource.net/schemas/serverbootstrap" xmlns:ns8="http://oec.api.opsource.net/schemas/manualimport" xmlns:ns11="http://oec.api.opsource.net/schemas/backup" xmlns:ns2="http://oec.api.opsource.net/schemas/directory" xmlns:ns4="http://oec.api.opsource.net/schemas/network" xmlns:ns3="http://oec.api.opsource.net/schemas/organization">
+    <ns6:operation>Change Backup Service Plan</ns6:operation>
+    <ns6:result>SUCCESS</ns6:result>
+    <ns6:resultDetail>Backup Service Plan changed</ns6:resultDetail>
+    <ns6:resultCode>REASON_0</ns6:resultCode>
+</ns6:Status>
diff --git a/libcloud/test/backup/test_dimensiondata.py b/libcloud/test/backup/test_dimensiondata.py
--- a/libcloud/test/backup/test_dimensiondata.py
+++ b/libcloud/test/backup/test_dimensiondata.py
@@ -1,490 +1,490 @@
-# 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.
-
-try:
-    from lxml import etree as ET
-except ImportError:
-    from xml.etree import ElementTree as ET
-
-import sys
-from libcloud.utils.py3 import httplib
-
-from libcloud.common.dimensiondata import DimensionDataAPIException
-from libcloud.common.types import InvalidCredsError
-from libcloud.backup.base import BackupTargetJob
-from libcloud.backup.drivers.dimensiondata import DimensionDataBackupDriver as DimensionData
-from libcloud.backup.drivers.dimensiondata import DEFAULT_BACKUP_PLAN
-
-from libcloud.test import MockHttp, unittest
-from libcloud.test.backup import TestCaseMixin
-from libcloud.test.file_fixtures import BackupFileFixtures
-
-from libcloud.test.secrets import DIMENSIONDATA_PARAMS
-
-
-class DimensionDataTests(unittest.TestCase, TestCaseMixin):
-
-    def setUp(self):
-        DimensionData.connectionCls.conn_classes = (None, DimensionDataMockHttp)
-        DimensionDataMockHttp.type = None
-        self.driver = DimensionData(*DIMENSIONDATA_PARAMS)
-
-    def test_invalid_region(self):
-        with self.assertRaises(ValueError):
-            self.driver = DimensionData(*DIMENSIONDATA_PARAMS, region='blah')
-
-    def test_invalid_creds(self):
-        DimensionDataMockHttp.type = 'UNAUTHORIZED'
-        with self.assertRaises(InvalidCredsError):
-            self.driver.list_targets()
-
-    def test_list_targets(self):
-        targets = self.driver.list_targets()
-        self.assertEqual(len(targets), 2)
-        self.assertEqual(targets[0].id, '5579f3a7-4c32-4cf5-8a7e-b45c36a35c10')
-        self.assertEqual(targets[0].address, 'e75ead52-692f-4314-8725-c8a4f4d13a87')
-        self.assertEqual(targets[0].extra['servicePlan'], 'Enterprise')
-
-    def test_create_target(self):
-        target = self.driver.create_target(
-            'name',
-            'e75ead52-692f-4314-8725-c8a4f4d13a87',
-            extra={'servicePlan': 'Enterprise'})
-        self.assertEqual(target.id, 'ee7c4b64-f7af-4a4f-8384-be362273530f')
-        self.assertEqual(target.address, 'e75ead52-692f-4314-8725-c8a4f4d13a87')
-        self.assertEqual(target.extra['servicePlan'], 'Enterprise')
-
-    def test_create_target_DEFAULT(self):
-        DimensionDataMockHttp.type = 'DEFAULT'
-        target = self.driver.create_target(
-            'name',
-            'e75ead52-692f-4314-8725-c8a4f4d13a87')
-        self.assertEqual(target.id, 'ee7c4b64-f7af-4a4f-8384-be362273530f')
-        self.assertEqual(target.address, 'e75ead52-692f-4314-8725-c8a4f4d13a87')
-
-    def test_create_target_EXISTS(self):
-        DimensionDataMockHttp.type = 'EXISTS'
-        with self.assertRaises(DimensionDataAPIException) as context:
-            self.driver.create_target(
-                'name',
-                'e75ead52-692f-4314-8725-c8a4f4d13a87',
-                extra={'servicePlan': 'Enterprise'})
-        self.assertEqual(context.exception.code, 'ERROR')
-        self.assertEqual(context.exception.msg, 'Cloud backup for this server is already enabled or being enabled (state: NORMAL).')
-
-    def test_update_target(self):
-        target = self.driver.list_targets()[0]
-        extra = {'servicePlan': 'Essentials'}
-        new_target = self.driver.update_target(target, extra=extra)
-        self.assertEqual(new_target.extra['servicePlan'], 'Essentials')
-
-    def test_update_target_DEFAULT(self):
-        DimensionDataMockHttp.type = 'DEFAULT'
-        target = 'e75ead52-692f-4314-8725-c8a4f4d13a87'
-        self.driver.update_target(target)
-
-    def test_update_target_STR(self):
-        target = 'e75ead52-692f-4314-8725-c8a4f4d13a87'
-        extra = {'servicePlan': 'Essentials'}
-        new_target = self.driver.update_target(target, extra=extra)
-        self.assertEqual(new_target.extra['servicePlan'], 'Essentials')
-
-    def test_delete_target(self):
-        target = self.driver.list_targets()[0]
-        self.assertTrue(self.driver.delete_target(target))
-
-    def test_ex_add_client_to_target(self):
-        target = self.driver.list_targets()[0]
-        client = self.driver.ex_list_available_client_types(target)[0]
-        storage_policy = self.driver.ex_list_available_storage_policies(target)[0]
-        schedule_policy = self.driver.ex_list_available_schedule_policies(target)[0]
-        self.assertTrue(
-            self.driver.ex_add_client_to_target(target, client, storage_policy,
-                                                schedule_policy, 'ON_FAILURE', 'nobody@example.com')
-        )
-
-    def test_ex_add_client_to_target_STR(self):
-        self.assertTrue(
-            self.driver.ex_add_client_to_target('e75ead52-692f-4314-8725-c8a4f4d13a87', 'FA.Linux', '14 Day Storage Policy',
-                                                '12AM - 6AM', 'ON_FAILURE', 'nobody@example.com')
-        )
-
-    def test_ex_get_backup_details_for_target(self):
-        target = self.driver.list_targets()[0]
-        response = self.driver.ex_get_backup_details_for_target(target)
-        self.assertEqual(response.service_plan, 'Enterprise')
-        client = response.clients[0]
-        self.assertEqual(client.id, '30b1ff76-c76d-4d7c-b39d-3b72be0384c8')
-        self.assertEqual(client.type.type, 'FA.Linux')
-        self.assertEqual(client.running_job.progress, 5)
-        self.assertTrue(isinstance(client.running_job, BackupTargetJob))
-        self.assertEqual(len(client.alert.notify_list), 2)
-        self.assertTrue(isinstance(client.alert.notify_list, list))
-
-    def test_ex_cancel_target_job(self):
-        target = self.driver.list_targets()[0]
-        response = self.driver.ex_get_backup_details_for_target(target)
-        client = response.clients[0]
-        self.assertTrue(isinstance(client.running_job, BackupTargetJob))
-        success = client.running_job.cancel()
-        self.assertTrue(success)
-
-    def test_ex_cancel_target_job_with_extras(self):
-        success = self.driver.cancel_target_job(
-            None,
-            ex_client='30b1ff76_c76d_4d7c_b39d_3b72be0384c8',
-            ex_target='e75ead52_692f_4314_8725_c8a4f4d13a87'
-        )
-        self.assertTrue(success)
-
-    def test_ex_cancel_target_job_FAIL(self):
-        DimensionDataMockHttp.type = 'FAIL'
-        with self.assertRaises(DimensionDataAPIException) as context:
-            self.driver.cancel_target_job(
-                None,
-                ex_client='30b1ff76_c76d_4d7c_b39d_3b72be0384c8',
-                ex_target='e75ead52_692f_4314_8725_c8a4f4d13a87'
-            )
-        self.assertEqual(context.exception.code, 'ERROR')
-
-    """Test a backup info for a target that does not have a client"""
-    def test_ex_get_backup_details_for_target_NO_CLIENT(self):
-        DimensionDataMockHttp.type = 'NOCLIENT'
-        response = self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314-8725-c8a4f4d13a87')
-        self.assertEqual(response.service_plan, 'Essentials')
-        self.assertEqual(len(response.clients), 0)
-
-    """Test a backup details that has a client, but no alerting or running jobs"""
-    def test_ex_get_backup_details_for_target_NO_JOB_OR_ALERT(self):
-        DimensionDataMockHttp.type = 'NOJOB'
-        response = self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314_8725-c8a4f4d13a87')
-        self.assertEqual(response.service_plan, 'Enterprise')
-        self.assertTrue(isinstance(response.clients, list))
-        self.assertEqual(len(response.clients), 1)
-        client = response.clients[0]
-        self.assertEqual(client.id, '30b1ff76-c76d-4d7c-b39d-3b72be0384c8')
-        self.assertEqual(client.type.type, 'FA.Linux')
-        self.assertIsNone(client.running_job)
-        self.assertIsNone(client.alert)
-
-    """Test getting backup info for a server that doesn't exist"""
-    def test_ex_get_backup_details_for_target_DISABLED(self):
-        DimensionDataMockHttp.type = 'DISABLED'
-        with self.assertRaises(DimensionDataAPIException) as context:
-            self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314-8725-c8a4f4d13a87')
-        self.assertEqual(context.exception.code, 'ERROR')
-        self.assertEqual(context.exception.msg, 'Server e75ead52-692f-4314-8725-c8a4f4d13a87 has not been provisioned for backup')
-
-    def test_ex_list_available_client_types(self):
-        target = self.driver.list_targets()[0]
-        answer = self.driver.ex_list_available_client_types(target)
-        self.assertEqual(len(answer), 1)
-        self.assertEqual(answer[0].type, 'FA.Linux')
-        self.assertEqual(answer[0].is_file_system, True)
-        self.assertEqual(answer[0].description, 'Linux File system')
-
-    def test_ex_list_available_storage_policies(self):
-        target = self.driver.list_targets()[0]
-        answer = self.driver.ex_list_available_storage_policies(target)
-        self.assertEqual(len(answer), 1)
-        self.assertEqual(answer[0].name,
-                         '30 Day Storage Policy + Secondary Copy')
-        self.assertEqual(answer[0].retention_period, 30)
-        self.assertEqual(answer[0].secondary_location, 'Primary')
-
-    def test_ex_list_available_schedule_policies(self):
-        target = self.driver.list_targets()[0]
-        answer = self.driver.ex_list_available_schedule_policies(target)
-        self.assertEqual(len(answer), 1)
-        self.assertEqual(answer[0].name, '12AM - 6AM')
-        self.assertEqual(answer[0].description, 'Daily backup will start between 12AM - 6AM')
-
-    def test_ex_remove_client_from_target(self):
-        target = self.driver.list_targets()[0]
-        client = self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314-8725-c8a4f4d13a87').clients[0]
-        self.assertTrue(self.driver.ex_remove_client_from_target(target, client))
-
-    def test_ex_remove_client_from_target_STR(self):
-        self.assertTrue(
-            self.driver.ex_remove_client_from_target(
-                'e75ead52-692f-4314-8725-c8a4f4d13a87',
-                '30b1ff76-c76d-4d7c-b39d-3b72be0384c8'
-            )
-        )
-
-    def test_ex_remove_client_from_target_FAIL(self):
-        DimensionDataMockHttp.type = 'FAIL'
-        with self.assertRaises(DimensionDataAPIException) as context:
-            self.driver.ex_remove_client_from_target(
-                'e75ead52-692f-4314-8725-c8a4f4d13a87',
-                '30b1ff76-c76d-4d7c-b39d-3b72be0384c8'
-            )
-        self.assertEqual(context.exception.code, 'ERROR')
-        self.assertTrue('Backup Client is currently performing another operation' in context.exception.msg)
-
-    def test_priv_target_to_target_address(self):
-        target = self.driver.list_targets()[0]
-        self.assertEqual(
-            self.driver._target_to_target_address(target),
-            'e75ead52-692f-4314-8725-c8a4f4d13a87'
-        )
-
-    def test_priv_target_to_target_address_STR(self):
-        self.assertEqual(
-            self.driver._target_to_target_address('e75ead52-692f-4314-8725-c8a4f4d13a87'),
-            'e75ead52-692f-4314-8725-c8a4f4d13a87'
-        )
-
-    def test_priv_target_to_target_address_TYPEERROR(self):
-        with self.assertRaises(TypeError):
-            self.driver._target_to_target_address([1, 2, 3])
-
-    def test_priv_client_to_client_id(self):
-        client = self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314-8725-c8a4f4d13a87').clients[0]
-        self.assertEqual(
-            self.driver._client_to_client_id(client),
-            '30b1ff76-c76d-4d7c-b39d-3b72be0384c8'
-        )
-
-    def test_priv_client_to_client_id_STR(self):
-        self.assertEqual(
-            self.driver._client_to_client_id('30b1ff76-c76d-4d7c-b39d-3b72be0384c8'),
-            '30b1ff76-c76d-4d7c-b39d-3b72be0384c8'
-        )
-
-    def test_priv_client_to_client_id_TYPEERROR(self):
-        with self.assertRaises(TypeError):
-            self.driver._client_to_client_id([1, 2, 3])
-
-
-class InvalidRequestError(Exception):
-    def __init__(self, tag):
-        super(InvalidRequestError, self).__init__("Invalid Request - %s" % tag)
-
-
-class DimensionDataMockHttp(MockHttp):
-
-    fixtures = BackupFileFixtures('dimensiondata')
-
-    def _oec_0_9_myaccount_UNAUTHORIZED(self, method, url, body, headers):
-        return (httplib.UNAUTHORIZED, "", {}, httplib.responses[httplib.UNAUTHORIZED])
-
-    def _oec_0_9_myaccount(self, method, url, body, headers):
-        body = self.fixtures.load('oec_0_9_myaccount.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_myaccount_EXISTS(self, method, url, body, headers):
-        body = self.fixtures.load('oec_0_9_myaccount.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_myaccount_DEFAULT(self, method, url, body, headers):
-        body = self.fixtures.load('oec_0_9_myaccount.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_myaccount_INPROGRESS(self, method, url, body, headers):
-        body = self.fixtures.load('oec_0_9_myaccount.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_myaccount_FAIL(self, method, url, body, headers):
-        body = self.fixtures.load('oec_0_9_myaccount.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_myaccount_NOCLIENT(self, method, url, body, headers):
-        body = self.fixtures.load('oec_0_9_myaccount.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_myaccount_DISABLED(self, method, url, body, headers):
-        body = self.fixtures.load('oec_0_9_myaccount.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_myaccount_NOJOB(self, method, url, body, headers):
-        body = self.fixtures.load('oec_0_9_myaccount.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87(self, method, url, body, headers):
-        body = self.fixtures.load(
-            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT(self, method, url, body, headers):
-        body = self.fixtures.load(
-            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_NOCLIENT(self, method, url, body, headers):
-        body = self.fixtures.load(
-            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_NOJOB(self, method, url, body, headers):
-        body = self.fixtures.load(
-            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DISABLED(self, method, url, body, headers):
-        body = self.fixtures.load(
-            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server(self, method, url, body, headers):
-        body = self.fixtures.load(
-            'server_server.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_type(self, method, url, body, headers):
-        body = self.fixtures.load(
-            '_backup_client_type.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_storagePolicy(
-            self, method, url, body, headers):
-        body = self.fixtures.load(
-            '_backup_client_storagePolicy.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_schedulePolicy(
-            self, method, url, body, headers):
-        body = self.fixtures.load(
-            '_backup_client_schedulePolicy.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client(
-            self, method, url, body, headers):
-        if method == 'POST':
-            body = self.fixtures.load(
-                '_backup_client_SUCCESS_PUT.xml')
-            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-        else:
-            raise ValueError("Unknown Method {0}".format(method))
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_NOCLIENT(
-            self, method, url, body, headers):
-        # only gets here are implemented
-        # If we get any other method something has gone wrong
-        assert(method == 'GET')
-        body = self.fixtures.load(
-            '_backup_INFO_NOCLIENT.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_DISABLED(
-            self, method, url, body, headers):
-        # only gets here are implemented
-        # If we get any other method something has gone wrong
-        assert(method == 'GET')
-        body = self.fixtures.load(
-            '_backup_INFO_DISABLED.xml')
-        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_NOJOB(
-            self, method, url, body, headers):
-        # only gets here are implemented
-        # If we get any other method something has gone wrong
-        assert(method == 'GET')
-        body = self.fixtures.load(
-            '_backup_INFO_NOJOB.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_DEFAULT(
-            self, method, url, body, headers):
-        if method != 'POST':
-            raise InvalidRequestError('Only POST is accepted for this test')
-        request = ET.fromstring(body)
-        service_plan = request.get('servicePlan')
-        if service_plan != DEFAULT_BACKUP_PLAN:
-            raise InvalidRequestError('The default plan %s should have been passed in.  Not %s' % (DEFAULT_BACKUP_PLAN, service_plan))
-        body = self.fixtures.load(
-            '_backup_ENABLE.xml')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup(
-            self, method, url, body, headers):
-        if method == 'POST':
-            body = self.fixtures.load(
-                '_backup_ENABLE.xml')
-            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-        elif method == 'GET':
-            if url.endswith('disable'):
-                body = self.fixtures.load(
-                    '_backup_DISABLE.xml')
-                return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-            body = self.fixtures.load(
-                '_backup_INFO.xml')
-            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-        else:
-            raise ValueError("Unknown Method {0}".format(method))
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_EXISTS(
-            self, method, url, body, headers):
-        # only POSTs are implemented
-        # If we get any other method something has gone wrong
-        assert(method == 'POST')
-        body = self.fixtures.load(
-            '_backup_EXISTS.xml')
-        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify(
-            self, method, url, body, headers):
-        request = ET.fromstring(body)
-        service_plan = request.get('servicePlan')
-        if service_plan != 'Essentials':
-            raise InvalidRequestError("Expected Essentials backup plan in request")
-        body = self.fixtures.load('_backup_modify.xml')
-
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify_DEFAULT(
-            self, method, url, body, headers):
-        request = ET.fromstring(body)
-        service_plan = request.get('servicePlan')
-        if service_plan != DEFAULT_BACKUP_PLAN:
-            raise InvalidRequestError("Expected % backup plan in test" % DEFAULT_BACKUP_PLAN)
-        body = self.fixtures.load('_backup_modify.xml')
-
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8(
-            self, method, url, body, headers):
-        if url.endswith('disable'):
-            body = self.fixtures.load(
-                ('_remove_backup_client.xml')
-            )
-        elif url.endswith('cancelJob'):
-            body = self.fixtures.load(
-                (''
-                 '_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob.xml')
-            )
-        else:
-            raise ValueError("Unknown URL: %s" % url)
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_FAIL(
-            self, method, url, body, headers):
-        if url.endswith('disable'):
-            body = self.fixtures.load(
-                ('_remove_backup_client_FAIL.xml')
-            )
-        elif url.endswith('cancelJob'):
-            body = self.fixtures.load(
-                (''
-                 '_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob_FAIL.xml')
-            )
-        else:
-            raise ValueError("Unknown URL: %s" % url)
-        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK])
-
-
-if __name__ == '__main__':
-    sys.exit(unittest.main())
+# 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.
+
+try:
+    from lxml import etree as ET
+except ImportError:
+    from xml.etree import ElementTree as ET
+
+import sys
+from libcloud.utils.py3 import httplib
+
+from libcloud.common.dimensiondata import DimensionDataAPIException
+from libcloud.common.types import InvalidCredsError
+from libcloud.backup.base import BackupTargetJob
+from libcloud.backup.drivers.dimensiondata import DimensionDataBackupDriver as DimensionData
+from libcloud.backup.drivers.dimensiondata import DEFAULT_BACKUP_PLAN
+
+from libcloud.test import MockHttp, unittest
+from libcloud.test.backup import TestCaseMixin
+from libcloud.test.file_fixtures import BackupFileFixtures
+
+from libcloud.test.secrets import DIMENSIONDATA_PARAMS
+
+
+class DimensionDataTests(unittest.TestCase, TestCaseMixin):
+
+    def setUp(self):
+        DimensionData.connectionCls.conn_classes = (None, DimensionDataMockHttp)
+        DimensionDataMockHttp.type = None
+        self.driver = DimensionData(*DIMENSIONDATA_PARAMS)
+
+    def test_invalid_region(self):
+        with self.assertRaises(ValueError):
+            self.driver = DimensionData(*DIMENSIONDATA_PARAMS, region='blah')
+
+    def test_invalid_creds(self):
+        DimensionDataMockHttp.type = 'UNAUTHORIZED'
+        with self.assertRaises(InvalidCredsError):
+            self.driver.list_targets()
+
+    def test_list_targets(self):
+        targets = self.driver.list_targets()
+        self.assertEqual(len(targets), 2)
+        self.assertEqual(targets[0].id, '5579f3a7-4c32-4cf5-8a7e-b45c36a35c10')
+        self.assertEqual(targets[0].address, 'e75ead52-692f-4314-8725-c8a4f4d13a87')
+        self.assertEqual(targets[0].extra['servicePlan'], 'Enterprise')
+
+    def test_create_target(self):
+        target = self.driver.create_target(
+            'name',
+            'e75ead52-692f-4314-8725-c8a4f4d13a87',
+            extra={'servicePlan': 'Enterprise'})
+        self.assertEqual(target.id, 'ee7c4b64-f7af-4a4f-8384-be362273530f')
+        self.assertEqual(target.address, 'e75ead52-692f-4314-8725-c8a4f4d13a87')
+        self.assertEqual(target.extra['servicePlan'], 'Enterprise')
+
+    def test_create_target_DEFAULT(self):
+        DimensionDataMockHttp.type = 'DEFAULT'
+        target = self.driver.create_target(
+            'name',
+            'e75ead52-692f-4314-8725-c8a4f4d13a87')
+        self.assertEqual(target.id, 'ee7c4b64-f7af-4a4f-8384-be362273530f')
+        self.assertEqual(target.address, 'e75ead52-692f-4314-8725-c8a4f4d13a87')
+
+    def test_create_target_EXISTS(self):
+        DimensionDataMockHttp.type = 'EXISTS'
+        with self.assertRaises(DimensionDataAPIException) as context:
+            self.driver.create_target(
+                'name',
+                'e75ead52-692f-4314-8725-c8a4f4d13a87',
+                extra={'servicePlan': 'Enterprise'})
+        self.assertEqual(context.exception.code, 'ERROR')
+        self.assertEqual(context.exception.msg, 'Cloud backup for this server is already enabled or being enabled (state: NORMAL).')
+
+    def test_update_target(self):
+        target = self.driver.list_targets()[0]
+        extra = {'servicePlan': 'Essentials'}
+        new_target = self.driver.update_target(target, extra=extra)
+        self.assertEqual(new_target.extra['servicePlan'], 'Essentials')
+
+    def test_update_target_DEFAULT(self):
+        DimensionDataMockHttp.type = 'DEFAULT'
+        target = 'e75ead52-692f-4314-8725-c8a4f4d13a87'
+        self.driver.update_target(target)
+
+    def test_update_target_STR(self):
+        target = 'e75ead52-692f-4314-8725-c8a4f4d13a87'
+        extra = {'servicePlan': 'Essentials'}
+        new_target = self.driver.update_target(target, extra=extra)
+        self.assertEqual(new_target.extra['servicePlan'], 'Essentials')
+
+    def test_delete_target(self):
+        target = self.driver.list_targets()[0]
+        self.assertTrue(self.driver.delete_target(target))
+
+    def test_ex_add_client_to_target(self):
+        target = self.driver.list_targets()[0]
+        client = self.driver.ex_list_available_client_types(target)[0]
+        storage_policy = self.driver.ex_list_available_storage_policies(target)[0]
+        schedule_policy = self.driver.ex_list_available_schedule_policies(target)[0]
+        self.assertTrue(
+            self.driver.ex_add_client_to_target(target, client, storage_policy,
+                                                schedule_policy, 'ON_FAILURE', 'nobody@example.com')
+        )
+
+    def test_ex_add_client_to_target_STR(self):
+        self.assertTrue(
+            self.driver.ex_add_client_to_target('e75ead52-692f-4314-8725-c8a4f4d13a87', 'FA.Linux', '14 Day Storage Policy',
+                                                '12AM - 6AM', 'ON_FAILURE', 'nobody@example.com')
+        )
+
+    def test_ex_get_backup_details_for_target(self):
+        target = self.driver.list_targets()[0]
+        response = self.driver.ex_get_backup_details_for_target(target)
+        self.assertEqual(response.service_plan, 'Enterprise')
+        client = response.clients[0]
+        self.assertEqual(client.id, '30b1ff76-c76d-4d7c-b39d-3b72be0384c8')
+        self.assertEqual(client.type.type, 'FA.Linux')
+        self.assertEqual(client.running_job.progress, 5)
+        self.assertTrue(isinstance(client.running_job, BackupTargetJob))
+        self.assertEqual(len(client.alert.notify_list), 2)
+        self.assertTrue(isinstance(client.alert.notify_list, list))
+
+    def test_ex_cancel_target_job(self):
+        target = self.driver.list_targets()[0]
+        response = self.driver.ex_get_backup_details_for_target(target)
+        client = response.clients[0]
+        self.assertTrue(isinstance(client.running_job, BackupTargetJob))
+        success = client.running_job.cancel()
+        self.assertTrue(success)
+
+    def test_ex_cancel_target_job_with_extras(self):
+        success = self.driver.cancel_target_job(
+            None,
+            ex_client='30b1ff76_c76d_4d7c_b39d_3b72be0384c8',
+            ex_target='e75ead52_692f_4314_8725_c8a4f4d13a87'
+        )
+        self.assertTrue(success)
+
+    def test_ex_cancel_target_job_FAIL(self):
+        DimensionDataMockHttp.type = 'FAIL'
+        with self.assertRaises(DimensionDataAPIException) as context:
+            self.driver.cancel_target_job(
+                None,
+                ex_client='30b1ff76_c76d_4d7c_b39d_3b72be0384c8',
+                ex_target='e75ead52_692f_4314_8725_c8a4f4d13a87'
+            )
+        self.assertEqual(context.exception.code, 'ERROR')
+
+    """Test a backup info for a target that does not have a client"""
+    def test_ex_get_backup_details_for_target_NO_CLIENT(self):
+        DimensionDataMockHttp.type = 'NOCLIENT'
+        response = self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314-8725-c8a4f4d13a87')
+        self.assertEqual(response.service_plan, 'Essentials')
+        self.assertEqual(len(response.clients), 0)
+
+    """Test a backup details that has a client, but no alerting or running jobs"""
+    def test_ex_get_backup_details_for_target_NO_JOB_OR_ALERT(self):
+        DimensionDataMockHttp.type = 'NOJOB'
+        response = self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314_8725-c8a4f4d13a87')
+        self.assertEqual(response.service_plan, 'Enterprise')
+        self.assertTrue(isinstance(response.clients, list))
+        self.assertEqual(len(response.clients), 1)
+        client = response.clients[0]
+        self.assertEqual(client.id, '30b1ff76-c76d-4d7c-b39d-3b72be0384c8')
+        self.assertEqual(client.type.type, 'FA.Linux')
+        self.assertIsNone(client.running_job)
+        self.assertIsNone(client.alert)
+
+    """Test getting backup info for a server that doesn't exist"""
+    def test_ex_get_backup_details_for_target_DISABLED(self):
+        DimensionDataMockHttp.type = 'DISABLED'
+        with self.assertRaises(DimensionDataAPIException) as context:
+            self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314-8725-c8a4f4d13a87')
+        self.assertEqual(context.exception.code, 'ERROR')
+        self.assertEqual(context.exception.msg, 'Server e75ead52-692f-4314-8725-c8a4f4d13a87 has not been provisioned for backup')
+
+    def test_ex_list_available_client_types(self):
+        target = self.driver.list_targets()[0]
+        answer = self.driver.ex_list_available_client_types(target)
+        self.assertEqual(len(answer), 1)
+        self.assertEqual(answer[0].type, 'FA.Linux')
+        self.assertEqual(answer[0].is_file_system, True)
+        self.assertEqual(answer[0].description, 'Linux File system')
+
+    def test_ex_list_available_storage_policies(self):
+        target = self.driver.list_targets()[0]
+        answer = self.driver.ex_list_available_storage_policies(target)
+        self.assertEqual(len(answer), 1)
+        self.assertEqual(answer[0].name,
+                         '30 Day Storage Policy + Secondary Copy')
+        self.assertEqual(answer[0].retention_period, 30)
+        self.assertEqual(answer[0].secondary_location, 'Primary')
+
+    def test_ex_list_available_schedule_policies(self):
+        target = self.driver.list_targets()[0]
+        answer = self.driver.ex_list_available_schedule_policies(target)
+        self.assertEqual(len(answer), 1)
+        self.assertEqual(answer[0].name, '12AM - 6AM')
+        self.assertEqual(answer[0].description, 'Daily backup will start between 12AM - 6AM')
+
+    def test_ex_remove_client_from_target(self):
+        target = self.driver.list_targets()[0]
+        client = self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314-8725-c8a4f4d13a87').clients[0]
+        self.assertTrue(self.driver.ex_remove_client_from_target(target, client))
+
+    def test_ex_remove_client_from_target_STR(self):
+        self.assertTrue(
+            self.driver.ex_remove_client_from_target(
+                'e75ead52-692f-4314-8725-c8a4f4d13a87',
+                '30b1ff76-c76d-4d7c-b39d-3b72be0384c8'
+            )
+        )
+
+    def test_ex_remove_client_from_target_FAIL(self):
+        DimensionDataMockHttp.type = 'FAIL'
+        with self.assertRaises(DimensionDataAPIException) as context:
+            self.driver.ex_remove_client_from_target(
+                'e75ead52-692f-4314-8725-c8a4f4d13a87',
+                '30b1ff76-c76d-4d7c-b39d-3b72be0384c8'
+            )
+        self.assertEqual(context.exception.code, 'ERROR')
+        self.assertTrue('Backup Client is currently performing another operation' in context.exception.msg)
+
+    def test_priv_target_to_target_address(self):
+        target = self.driver.list_targets()[0]
+        self.assertEqual(
+            self.driver._target_to_target_address(target),
+            'e75ead52-692f-4314-8725-c8a4f4d13a87'
+        )
+
+    def test_priv_target_to_target_address_STR(self):
+        self.assertEqual(
+            self.driver._target_to_target_address('e75ead52-692f-4314-8725-c8a4f4d13a87'),
+            'e75ead52-692f-4314-8725-c8a4f4d13a87'
+        )
+
+    def test_priv_target_to_target_address_TYPEERROR(self):
+        with self.assertRaises(TypeError):
+            self.driver._target_to_target_address([1, 2, 3])
+
+    def test_priv_client_to_client_id(self):
+        client = self.driver.ex_get_backup_details_for_target('e75ead52-692f-4314-8725-c8a4f4d13a87').clients[0]
+        self.assertEqual(
+            self.driver._client_to_client_id(client),
+            '30b1ff76-c76d-4d7c-b39d-3b72be0384c8'
+        )
+
+    def test_priv_client_to_client_id_STR(self):
+        self.assertEqual(
+            self.driver._client_to_client_id('30b1ff76-c76d-4d7c-b39d-3b72be0384c8'),
+            '30b1ff76-c76d-4d7c-b39d-3b72be0384c8'
+        )
+
+    def test_priv_client_to_client_id_TYPEERROR(self):
+        with self.assertRaises(TypeError):
+            self.driver._client_to_client_id([1, 2, 3])
+
+
+class InvalidRequestError(Exception):
+    def __init__(self, tag):
+        super(InvalidRequestError, self).__init__("Invalid Request - %s" % tag)
+
+
+class DimensionDataMockHttp(MockHttp):
+
+    fixtures = BackupFileFixtures('dimensiondata')
+
+    def _oec_0_9_myaccount_UNAUTHORIZED(self, method, url, body, headers):
+        return (httplib.UNAUTHORIZED, "", {}, httplib.responses[httplib.UNAUTHORIZED])
+
+    def _oec_0_9_myaccount(self, method, url, body, headers):
+        body = self.fixtures.load('oec_0_9_myaccount.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_myaccount_EXISTS(self, method, url, body, headers):
+        body = self.fixtures.load('oec_0_9_myaccount.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_myaccount_DEFAULT(self, method, url, body, headers):
+        body = self.fixtures.load('oec_0_9_myaccount.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_myaccount_INPROGRESS(self, method, url, body, headers):
+        body = self.fixtures.load('oec_0_9_myaccount.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_myaccount_FAIL(self, method, url, body, headers):
+        body = self.fixtures.load('oec_0_9_myaccount.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_myaccount_NOCLIENT(self, method, url, body, headers):
+        body = self.fixtures.load('oec_0_9_myaccount.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_myaccount_DISABLED(self, method, url, body, headers):
+        body = self.fixtures.load('oec_0_9_myaccount.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_myaccount_NOJOB(self, method, url, body, headers):
+        body = self.fixtures.load('oec_0_9_myaccount.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_NOCLIENT(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_NOJOB(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DISABLED(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'server_server_e75ead52_692f_4314_8725_c8a4f4d13a87_DEFAULT.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _caas_2_2_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_server(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'server_server.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_type(self, method, url, body, headers):
+        body = self.fixtures.load(
+            '_backup_client_type.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_storagePolicy(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+            '_backup_client_storagePolicy.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_schedulePolicy(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+            '_backup_client_schedulePolicy.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client(
+            self, method, url, body, headers):
+        if method == 'POST':
+            body = self.fixtures.load(
+                '_backup_client_SUCCESS_PUT.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+        else:
+            raise ValueError("Unknown Method {0}".format(method))
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_NOCLIENT(
+            self, method, url, body, headers):
+        # only gets here are implemented
+        # If we get any other method something has gone wrong
+        assert(method == 'GET')
+        body = self.fixtures.load(
+            '_backup_INFO_NOCLIENT.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_DISABLED(
+            self, method, url, body, headers):
+        # only gets here are implemented
+        # If we get any other method something has gone wrong
+        assert(method == 'GET')
+        body = self.fixtures.load(
+            '_backup_INFO_DISABLED.xml')
+        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_NOJOB(
+            self, method, url, body, headers):
+        # only gets here are implemented
+        # If we get any other method something has gone wrong
+        assert(method == 'GET')
+        body = self.fixtures.load(
+            '_backup_INFO_NOJOB.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_DEFAULT(
+            self, method, url, body, headers):
+        if method != 'POST':
+            raise InvalidRequestError('Only POST is accepted for this test')
+        request = ET.fromstring(body)
+        service_plan = request.get('servicePlan')
+        if service_plan != DEFAULT_BACKUP_PLAN:
+            raise InvalidRequestError('The default plan %s should have been passed in.  Not %s' % (DEFAULT_BACKUP_PLAN, service_plan))
+        body = self.fixtures.load(
+            '_backup_ENABLE.xml')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup(
+            self, method, url, body, headers):
+        if method == 'POST':
+            body = self.fixtures.load(
+                '_backup_ENABLE.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+        elif method == 'GET':
+            if url.endswith('disable'):
+                body = self.fixtures.load(
+                    '_backup_DISABLE.xml')
+                return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+            body = self.fixtures.load(
+                '_backup_INFO.xml')
+            return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+        else:
+            raise ValueError("Unknown Method {0}".format(method))
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_EXISTS(
+            self, method, url, body, headers):
+        # only POSTs are implemented
+        # If we get any other method something has gone wrong
+        assert(method == 'POST')
+        body = self.fixtures.load(
+            '_backup_EXISTS.xml')
+        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify(
+            self, method, url, body, headers):
+        request = ET.fromstring(body)
+        service_plan = request.get('servicePlan')
+        if service_plan != 'Essentials':
+            raise InvalidRequestError("Expected Essentials backup plan in request")
+        body = self.fixtures.load('_backup_modify.xml')
+
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_modify_DEFAULT(
+            self, method, url, body, headers):
+        request = ET.fromstring(body)
+        service_plan = request.get('servicePlan')
+        if service_plan != DEFAULT_BACKUP_PLAN:
+            raise InvalidRequestError("Expected % backup plan in test" % DEFAULT_BACKUP_PLAN)
+        body = self.fixtures.load('_backup_modify.xml')
+
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8(
+            self, method, url, body, headers):
+        if url.endswith('disable'):
+            body = self.fixtures.load(
+                ('_remove_backup_client.xml')
+            )
+        elif url.endswith('cancelJob'):
+            body = self.fixtures.load(
+                (''
+                 '_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob.xml')
+            )
+        else:
+            raise ValueError("Unknown URL: %s" % url)
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_server_e75ead52_692f_4314_8725_c8a4f4d13a87_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_FAIL(
+            self, method, url, body, headers):
+        if url.endswith('disable'):
+            body = self.fixtures.load(
+                ('_remove_backup_client_FAIL.xml')
+            )
+        elif url.endswith('cancelJob'):
+            body = self.fixtures.load(
+                (''
+                 '_backup_client_30b1ff76_c76d_4d7c_b39d_3b72be0384c8_cancelJob_FAIL.xml')
+            )
+        else:
+            raise ValueError("Unknown URL: %s" % url)
+        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())
diff --git a/libcloud/test/compute/fixtures/dimensiondata/detailed_usage_report.csv b/libcloud/test/compute/fixtures/dimensiondata/detailed_usage_report.csv
new file mode 100644
--- /dev/null
+++ b/libcloud/test/compute/fixtures/dimensiondata/detailed_usage_report.csv
@@ -0,0 +1,42 @@
+Name,Type,Location,Private IP
+Address,Status,"user:ATag","user:BTag","user:CTag",Start Time,End
+Time,Duration (Hours),CPU Type,CPU Count, RAM (GB), Storage (GB),Economy
+Storage (GB),CPU Hours,High Performance CPU Hours,RAM Hours,Storage
+Hours,Economy Storage Hours,Bandwidth-In (GB),Bandwidth-Out (GB),Subadmin
+Hours,Network Hours,Essentials Network Domain Hours,Advanced Network Domain
+Hours,VLAN Hours,Public IP Hours,Cloud Files Account Hours,Cloud Storage (GB)
+Server A,Server,NA1,10.162.12.16,Running,Bob,IT,,19/06/2014 04:16,19/06/2014
+11:17,7.03,Standard,1,2,10,0,7.03,0,14.05,70.27,0,0,0,0,0,0,0,0,0,0,0
+Server A,Server,NA1,10.162.12.16,Running,Bob,IT,,19/06/2014 11:17,20/06/2014
+00:00,12.71,High Performance,1,2,10,0,0,12.71,0,127.06,0,0,0,0,0,0,0,0,0,0,0
+Server B,Server,NA1,10.162.12.13,Running,Mary,IT,,19/06/2014 03:42,19/06/2014
+11:18,7.61,Essentials,1,2,10,0,0,0,15.22,76.1,0,0,0,0,0,0,0,0,0,0,0
+Server B,Server,NA1,10.162.12.13,Stopped,Mary,IT,,19/06/2014 11:18,19/06/2014
+23:18,12,Essentials,1,2,10,0,0,0,0,120,0,0,0,0,0,0,0,0,0,0,0
+Server B,Server,NA1,10.162.12.13,Stopped,Bob,IT,,19/06/2014 23:18,20/06/2014
+00:00,0.689722222,Essentials,1,2,10,0,0,0,0,6.897222223,0,0,0,0,0,0,0,0,0,0,0
+Customer Image A,Image,NA1,,Running,Bob,IT,,19/06/2014 03:30,20/06/2014
+00:00,20.5,,0,0,10,0,0,0,0,205,0,0,0,0,0,0,0,0,0,0,0
+Customer Image B,Image,NA1,,Running,Bob,IT,,19/06/2014 03:10,19/06/2014
+03:12,0.04,,0,0,10,0,0,0,0,0.44,0,0,0,0,0,0,0,0,0,0,0
+Joseph_F,Sub-Administrator,,,Running,Bob,IT,,19/06/2014 00:00,20/06/2014
+00:00,24,,0,0,0,0,0,0,0,0,0,0,0,24,0,0,0,0,0,0,0
+Lguy,Sub-Administrator,,,Running,Mary,IT,,19/06/2014 00:00,20/06/2014
+00:00,24,,0,0,0,0,0,0,0,0,0,0,0,24,0,0,0,0,0,0,0
+a network,Network,NA1,,Running,,IT,,19/06/2014 01:39,20/06/2014
+00:00,22.34,,0,0,0,0,0,0,0,0,0,0,0.9,0,22.34,0,0,0,0,0,0
+b network,Network,NA1,,Running,,IT,,19/06/2014 01:49,20/06/2014
+00:00,22.18,,0,0,0,0,0,0,0,0,0,0,7.1,0,22.18,0,0,0,0,0,0
+Rick's Network Domain,Network Domain,NA9,,Running,Bob,IT,,19/06/2014
+04:19,20/06/2014 00:00,19.68,,0,0,0,0,0,0,0,0,0,0,7.1,0,22.18,19.68,0,0,0,0,0
+Rick's VLAN,VLAN,NA9,,Running,Bob,IT,,19/06/2014 04:22,20/06/2014
+00:00,19.63,,0,0,0,0,0,0,0,0,0,0,0,0,22.18,0,0,19.63,0,0,0
+files1_koala,Cloud Files,,,Running,,IT,,19/06/2014 00:00,20/06/2014
+00:00,24,,0,0,0,0,0,0,0,0,0,0,0.5,0,0,0,0,0,0,24,52
+testbed,Cloud Files,,,Running,,IT,,19/06/2014 00:00,20/06/2014
+00:00,24,,0,0,0,0,0,0,0,0,0,0,0.01,0,0,0,0,0,0,24,100
+Network: b network IP Block:
+168.143.92.136(4),IpBlock,NA1,,Reserved,Mary,IT,,19/06/2014 11:47,20/06/2014 
+00:00,12.2,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48.82,0,0
+TOTAL for 19/06/14,,,,,,,,19/06/2014 00:00,20/06/2014
+00:00,24,,0,0,0,0,8,13,30,606,0,3,23,48,89,20,0,20,49,48,152
\ No newline at end of file
diff --git a/libcloud/test/compute/fixtures/dimensiondata/summary_usage_report.csv b/libcloud/test/compute/fixtures/dimensiondata/summary_usage_report.csv
new file mode 100644
--- /dev/null
+++ b/libcloud/test/compute/fixtures/dimensiondata/summary_usage_report.csv
@@ -0,0 +1,13 @@
+DAY,Location,CPU Hours,RAM Hours,Storage Hours,Performance Storage
+Hours,Bandwidth In,Bandwidth Out,Sub-Admin Hours,Network Hours,Public IP
+Hours,Cloud Files Account Hours,Cloud Files (GB Days),Software
+Units,Essentials Client Days,Advanced Client Days,Enterprise Client
+Days,Essentials Backups (GB),Advanced Backups (GB),Enterprise Backups (GB)
+01/01/2013,NA1,144,288,3360,264,0,0,72,24,0,0,0,0,0,0,0,0,0,0
+02/01/2013,NA1,144,288,3360,264,0,0,72,24,0,0,0,0,0,0,0,0,0,0
+03/01/2013,NA1,144,288,3360,264,0,0,72,24,0,0,0,0,0,0,0,0,0,0
+04/01/2013,NA1,144,288,3360,264,0,0,72,24,0,0,0,0,0,0,0,0,0,0
+05/01/2013,NA1,144,288,3360,264,0,0,72,24,0,0,0,0,0,0,0,0,0,0
+06/01/2013,NA1,172,344,3958,264,0,0,72,24,0,0,0,0,0,0,0,0,0,0
+07/01/2013,NA1,205,410,4783,264,0,0,72,24,0,0,0,0,0,0,0,0,0,0
+08/01/2013,NA1,216,432,5136,264,0,0,72,24,0,0,0,0,0,0,0,0,0,0
\ No newline at end of file
diff --git a/libcloud/test/compute/test_dimensiondata.py b/libcloud/test/compute/test_dimensiondata.py
--- a/libcloud/test/compute/test_dimensiondata.py
+++ b/libcloud/test/compute/test_dimensiondata.py
@@ -29,7 +29,7 @@
 from libcloud.common.dimensiondata import TYPES_URN
 from libcloud.compute.drivers.dimensiondata import DimensionDataNodeDriver as DimensionData
 from libcloud.compute.base import Node, NodeAuthPassword, NodeLocation
-from libcloud.test import MockHttp, unittest
+from libcloud.test import MockHttp, unittest, MockRawResponse, StorageMockHttp
 from libcloud.test.compute import TestCaseMixin
 from libcloud.test.file_fixtures import ComputeFileFixtures
 from libcloud.test.secrets import DIMENSIONDATA_PARAMS
@@ -40,6 +40,8 @@ class DimensionDataTests(unittest.TestCase, TestCaseMixin):
 
     def setUp(self):
         DimensionData.connectionCls.conn_classes = (None, DimensionDataMockHttp)
+        DimensionData.connectionCls.rawResponseCls = \
+            DimensionDataMockRawResponse
         DimensionDataMockHttp.type = None
         self.driver = DimensionData(*DIMENSIONDATA_PARAMS)
 
@@ -1148,13 +1150,41 @@ def test_priv_image_needs_auth_cust_img_linux_STR(self):
         image = self.driver.ex_list_customer_images()[0].id
         self.assertTrue(not self.driver._image_needs_auth(image))
 
+    def test_summary_usage_report(self):
+        report = self.driver.ex_summary_usage_report('2016-06-01', '2016-06-30')
+        report_content = report
+        self.assertEqual(len(report_content), 13)
+        self.assertEqual(len(report_content[0]), 6)
+
+    def test_detailed_usage_report(self):
+        report = self.driver.ex_detailed_usage_report('2016-06-01', '2016-06-30')
+        report_content = report
+        self.assertEqual(len(report_content), 42)
+        self.assertEqual(len(report_content[0]), 4)
+
 
 class InvalidRequestError(Exception):
     def __init__(self, tag):
         super(InvalidRequestError, self).__init__("Invalid Request - %s" % tag)
 
 
-class DimensionDataMockHttp(MockHttp):
+class DimensionDataMockRawResponse(MockRawResponse):
+    fixtures = ComputeFileFixtures('dimensiondata')
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_report_usage(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'summary_usage_report.csv'
+        )
+        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK])
+
+    def _oec_0_9_8a8f6abc_2745_4d8a_9cbc_8dabe5a7d0e4_report_usageDetailed(self, method, url, body, headers):
+        body = self.fixtures.load(
+            'detailed_usage_report.csv'
+        )
+        return (httplib.BAD_REQUEST, body, {}, httplib.responses[httplib.OK])
+
+
+class DimensionDataMockHttp(StorageMockHttp, MockHttp):
 
     fixtures = ComputeFileFixtures('dimensiondata')
 
diff --git a/libcloud/test/storage/test_backblaze_b2.py b/libcloud/test/storage/test_backblaze_b2.py
--- a/libcloud/test/storage/test_backblaze_b2.py
+++ b/libcloud/test/storage/test_backblaze_b2.py
@@ -1,238 +1,238 @@
-# 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 tempfile
-
-import mock
-
-from libcloud.storage.drivers.backblaze_b2 import BackblazeB2StorageDriver
-from libcloud.utils.py3 import httplib
-from libcloud.test import unittest
-from libcloud.test import StorageMockHttp
-from libcloud.test import MockRawResponse
-from libcloud.test import MockHttpTestCase
-from libcloud.test.file_fixtures import StorageFileFixtures
-
-
-class MockAuthConn(mock.Mock):
-    account_id = 'abcdefgh'
-
-
-class BackblazeB2StorageDriverTestCase(unittest.TestCase):
-    driver_klass = BackblazeB2StorageDriver
-    driver_args = ('a', 'b')
-
-    def setUp(self):
-        self.driver_klass.connectionCls.authCls = MockAuthConn()
-        self.driver_klass.connectionCls.conn_classes = (
-            None, BackblazeB2MockHttp)
-        self.driver_klass.connectionCls.rawResponseCls = \
-            BackblazeB2MockRawResponse
-        BackblazeB2MockHttp.type = None
-        BackblazeB2MockRawResponse.type = None
-        self.driver = self.driver_klass(*self.driver_args)
-
-    def test_list_containers(self):
-        containers = self.driver.list_containers()
-        self.assertEqual(len(containers), 3)
-        self.assertEqual(containers[0].name, 'test00001')
-        self.assertEqual(containers[0].extra['id'], '481c37de2e1ab3bf5e150710')
-        self.assertEqual(containers[0].extra['bucketType'], 'allPrivate')
-
-    def test_list_container_objects(self):
-        container = self.driver.list_containers()[0]
-        objects = self.driver.list_container_objects(container=container)
-        self.assertEqual(len(objects), 4)
-        self.assertEqual(objects[0].name, '2.txt')
-        self.assertEqual(objects[0].size, 2)
-        self.assertEqual(objects[0].extra['fileId'], 'abcd')
-        self.assertEqual(objects[0].extra['uploadTimestamp'], 1450545966000)
-
-    def test_get_container(self):
-        container = self.driver.get_container('test00001')
-        self.assertEqual(container.name, 'test00001')
-        self.assertEqual(container.extra['id'], '481c37de2e1ab3bf5e150710')
-        self.assertEqual(container.extra['bucketType'], 'allPrivate')
-
-    def test_get_object(self):
-        obj = self.driver.get_object('test00001', '2.txt')
-        self.assertEqual(obj.name, '2.txt')
-        self.assertEqual(obj.size, 2)
-        self.assertEqual(obj.extra['fileId'], 'abcd')
-        self.assertEqual(obj.extra['uploadTimestamp'], 1450545966000)
-
-    def test_create_container(self):
-        container = self.driver.create_container(container_name='test0005')
-        self.assertEqual(container.name, 'test0005')
-        self.assertEqual(container.extra['id'], '681c87aebeaa530f5e250710')
-        self.assertEqual(container.extra['bucketType'], 'allPrivate')
-
-    def test_delete_container(self):
-        container = self.driver.list_containers()[0]
-        result = self.driver.delete_container(container=container)
-        self.assertTrue(result)
-
-    def test_download_object(self):
-        container = self.driver.list_containers()[0]
-        obj = self.driver.list_container_objects(container=container)[0]
-        _, destination_path = tempfile.mkstemp()
-        result = self.driver.download_object(obj=obj, destination_path=destination_path,
-                                             overwrite_existing=True)
-        self.assertTrue(result)
-
-    def test_download_object_as_stream(self):
-        container = self.driver.list_containers()[0]
-        obj = self.driver.list_container_objects(container=container)[0]
-        result = self.driver.download_object_as_stream(obj=obj)
-        result = ''.join([x.decode('utf-8') for x in list(result)])
-        self.assertEqual(result, 'ab')
-
-    def test_upload_object(self):
-        file_path = os.path.abspath(__file__)
-        container = self.driver.list_containers()[0]
-        obj = self.driver.upload_object(file_path=file_path, container=container,
-                                        object_name='test0007.txt')
-        self.assertEqual(obj.name, 'test0007.txt')
-        self.assertEqual(obj.size, 24)
-        self.assertEqual(obj.extra['fileId'], 'abcde')
-
-    def test_upload_object_via_stream(self):
-        container = self.driver.list_containers()[0]
-        file_path = os.path.abspath(__file__)
-        file = open(file_path, 'rb')
-        iterator = iter(file)
-        obj = self.driver.upload_object_via_stream(iterator=iterator,
-                                                   container=container,
-                                                   object_name='test0007.txt')
-        self.assertEqual(obj.name, 'test0007.txt')
-        self.assertEqual(obj.size, 24)
-        self.assertEqual(obj.extra['fileId'], 'abcde')
-
-    def test_delete_object(self):
-        container = self.driver.list_containers()[0]
-        obj = self.driver.list_container_objects(container=container)[0]
-        result = self.driver.delete_object(obj=obj)
-        self.assertTrue(result)
-
-    def test_ex_hide_object(self):
-        container = self.driver.list_containers()[0]
-        container_id = container.extra['id']
-        obj = self.driver.ex_hide_object(container_id=container_id,
-                                         object_name='2.txt')
-        self.assertEqual(obj.name, '2.txt')
-
-    def test_ex_list_object_versions(self):
-        container = self.driver.list_containers()[0]
-        container_id = container.extra['id']
-        objects = self.driver.ex_list_object_versions(container_id=container_id)
-        self.assertEqual(len(objects), 9)
-
-    def test_ex_get_upload_data(self):
-        container = self.driver.list_containers()[0]
-        container_id = container.extra['id']
-        data = self.driver.ex_get_upload_data(container_id=container_id)
-        self.assertEqual(data['authorizationToken'], 'nope')
-        self.assertEqual(data['bucketId'], '481c37de2e1ab3bf5e150710')
-        self.assertEqual(data['uploadUrl'], 'https://podxxx.backblaze.com/b2api/v1/b2_upload_file/abcd/defg')
-
-    def test_ex_get_upload_url(self):
-        container = self.driver.list_containers()[0]
-        container_id = container.extra['id']
-        url = self.driver.ex_get_upload_url(container_id=container_id)
-        self.assertEqual(url, 'https://podxxx.backblaze.com/b2api/v1/b2_upload_file/abcd/defg')
-
-
-class BackblazeB2MockHttp(StorageMockHttp, MockHttpTestCase):
-    fixtures = StorageFileFixtures('backblaze_b2')
-
-    def _b2api_v1_b2_list_buckets(self, method, url, body, headers):
-        if method == 'GET':
-            body = self.fixtures.load('b2_list_buckets.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _b2api_v1_b2_list_file_names(self, method, url, body, headers):
-        if method == 'GET':
-            body = self.fixtures.load('b2_list_file_names.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _b2api_v1_b2_create_bucket(self, method, url, body, headers):
-        if method == 'POST':
-            body = self.fixtures.load('b2_create_bucket.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _b2api_v1_b2_delete_bucket(self, method, url, body, headers):
-        if method == 'POST':
-            body = self.fixtures.load('b2_delete_bucket.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _b2api_v1_b2_delete_file_version(self, method, url, body, headers):
-        if method == 'POST':
-            body = self.fixtures.load('b2_delete_file_version.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _b2api_v1_b2_get_upload_url(self, method, url, body, headers):
-        # test_upload_object
-        if method == 'GET':
-            body = self.fixtures.load('b2_get_upload_url.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _b2api_v1_b2_upload_file_abcd_defg(self, method, url, body, headers):
-        # test_upload_object
-        if method == 'POST':
-            body = self.fixtures.load('b2_upload_file.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _b2api_v1_b2_list_file_versions(self, method, url, body, headers):
-        if method == 'GET':
-            body = self.fixtures.load('b2_list_file_versions.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-    def _b2api_v1_b2_hide_file(self, method, url, body, headers):
-        if method == 'POST':
-            body = self.fixtures.load('b2_hide_file.json')
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-
-class BackblazeB2MockRawResponse(MockRawResponse):
-    def _file_test00001_2_txt(self, method, url, body, headers):
-        # test_download_object
-        if method == 'GET':
-            body = 'ab'
-        else:
-            raise AssertionError('Unsupported method')
-        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
-
-if __name__ == '__main__':
-    sys.exit(unittest.main())
+# 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 tempfile
+
+import mock
+
+from libcloud.storage.drivers.backblaze_b2 import BackblazeB2StorageDriver
+from libcloud.utils.py3 import httplib
+from libcloud.test import unittest
+from libcloud.test import StorageMockHttp
+from libcloud.test import MockRawResponse
+from libcloud.test import MockHttpTestCase
+from libcloud.test.file_fixtures import StorageFileFixtures
+
+
+class MockAuthConn(mock.Mock):
+    account_id = 'abcdefgh'
+
+
+class BackblazeB2StorageDriverTestCase(unittest.TestCase):
+    driver_klass = BackblazeB2StorageDriver
+    driver_args = ('a', 'b')
+
+    def setUp(self):
+        self.driver_klass.connectionCls.authCls = MockAuthConn()
+        self.driver_klass.connectionCls.conn_classes = (
+            None, BackblazeB2MockHttp)
+        self.driver_klass.connectionCls.rawResponseCls = \
+            BackblazeB2MockRawResponse
+        BackblazeB2MockHttp.type = None
+        BackblazeB2MockRawResponse.type = None
+        self.driver = self.driver_klass(*self.driver_args)
+
+    def test_list_containers(self):
+        containers = self.driver.list_containers()
+        self.assertEqual(len(containers), 3)
+        self.assertEqual(containers[0].name, 'test00001')
+        self.assertEqual(containers[0].extra['id'], '481c37de2e1ab3bf5e150710')
+        self.assertEqual(containers[0].extra['bucketType'], 'allPrivate')
+
+    def test_list_container_objects(self):
+        container = self.driver.list_containers()[0]
+        objects = self.driver.list_container_objects(container=container)
+        self.assertEqual(len(objects), 4)
+        self.assertEqual(objects[0].name, '2.txt')
+        self.assertEqual(objects[0].size, 2)
+        self.assertEqual(objects[0].extra['fileId'], 'abcd')
+        self.assertEqual(objects[0].extra['uploadTimestamp'], 1450545966000)
+
+    def test_get_container(self):
+        container = self.driver.get_container('test00001')
+        self.assertEqual(container.name, 'test00001')
+        self.assertEqual(container.extra['id'], '481c37de2e1ab3bf5e150710')
+        self.assertEqual(container.extra['bucketType'], 'allPrivate')
+
+    def test_get_object(self):
+        obj = self.driver.get_object('test00001', '2.txt')
+        self.assertEqual(obj.name, '2.txt')
+        self.assertEqual(obj.size, 2)
+        self.assertEqual(obj.extra['fileId'], 'abcd')
+        self.assertEqual(obj.extra['uploadTimestamp'], 1450545966000)
+
+    def test_create_container(self):
+        container = self.driver.create_container(container_name='test0005')
+        self.assertEqual(container.name, 'test0005')
+        self.assertEqual(container.extra['id'], '681c87aebeaa530f5e250710')
+        self.assertEqual(container.extra['bucketType'], 'allPrivate')
+
+    def test_delete_container(self):
+        container = self.driver.list_containers()[0]
+        result = self.driver.delete_container(container=container)
+        self.assertTrue(result)
+
+    def test_download_object(self):
+        container = self.driver.list_containers()[0]
+        obj = self.driver.list_container_objects(container=container)[0]
+        _, destination_path = tempfile.mkstemp()
+        result = self.driver.download_object(obj=obj, destination_path=destination_path,
+                                             overwrite_existing=True)
+        self.assertTrue(result)
+
+    def test_download_object_as_stream(self):
+        container = self.driver.list_containers()[0]
+        obj = self.driver.list_container_objects(container=container)[0]
+        result = self.driver.download_object_as_stream(obj=obj)
+        result = ''.join([x.decode('utf-8') for x in list(result)])
+        self.assertEqual(result, 'ab')
+
+    def test_upload_object(self):
+        file_path = os.path.abspath(__file__)
+        container = self.driver.list_containers()[0]
+        obj = self.driver.upload_object(file_path=file_path, container=container,
+                                        object_name='test0007.txt')
+        self.assertEqual(obj.name, 'test0007.txt')
+        self.assertEqual(obj.size, 24)
+        self.assertEqual(obj.extra['fileId'], 'abcde')
+
+    def test_upload_object_via_stream(self):
+        container = self.driver.list_containers()[0]
+        file_path = os.path.abspath(__file__)
+        file = open(file_path, 'rb')
+        iterator = iter(file)
+        obj = self.driver.upload_object_via_stream(iterator=iterator,
+                                                   container=container,
+                                                   object_name='test0007.txt')
+        self.assertEqual(obj.name, 'test0007.txt')
+        self.assertEqual(obj.size, 24)
+        self.assertEqual(obj.extra['fileId'], 'abcde')
+
+    def test_delete_object(self):
+        container = self.driver.list_containers()[0]
+        obj = self.driver.list_container_objects(container=container)[0]
+        result = self.driver.delete_object(obj=obj)
+        self.assertTrue(result)
+
+    def test_ex_hide_object(self):
+        container = self.driver.list_containers()[0]
+        container_id = container.extra['id']
+        obj = self.driver.ex_hide_object(container_id=container_id,
+                                         object_name='2.txt')
+        self.assertEqual(obj.name, '2.txt')
+
+    def test_ex_list_object_versions(self):
+        container = self.driver.list_containers()[0]
+        container_id = container.extra['id']
+        objects = self.driver.ex_list_object_versions(container_id=container_id)
+        self.assertEqual(len(objects), 9)
+
+    def test_ex_get_upload_data(self):
+        container = self.driver.list_containers()[0]
+        container_id = container.extra['id']
+        data = self.driver.ex_get_upload_data(container_id=container_id)
+        self.assertEqual(data['authorizationToken'], 'nope')
+        self.assertEqual(data['bucketId'], '481c37de2e1ab3bf5e150710')
+        self.assertEqual(data['uploadUrl'], 'https://podxxx.backblaze.com/b2api/v1/b2_upload_file/abcd/defg')
+
+    def test_ex_get_upload_url(self):
+        container = self.driver.list_containers()[0]
+        container_id = container.extra['id']
+        url = self.driver.ex_get_upload_url(container_id=container_id)
+        self.assertEqual(url, 'https://podxxx.backblaze.com/b2api/v1/b2_upload_file/abcd/defg')
+
+
+class BackblazeB2MockHttp(StorageMockHttp, MockHttpTestCase):
+    fixtures = StorageFileFixtures('backblaze_b2')
+
+    def _b2api_v1_b2_list_buckets(self, method, url, body, headers):
+        if method == 'GET':
+            body = self.fixtures.load('b2_list_buckets.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _b2api_v1_b2_list_file_names(self, method, url, body, headers):
+        if method == 'GET':
+            body = self.fixtures.load('b2_list_file_names.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _b2api_v1_b2_create_bucket(self, method, url, body, headers):
+        if method == 'POST':
+            body = self.fixtures.load('b2_create_bucket.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _b2api_v1_b2_delete_bucket(self, method, url, body, headers):
+        if method == 'POST':
+            body = self.fixtures.load('b2_delete_bucket.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _b2api_v1_b2_delete_file_version(self, method, url, body, headers):
+        if method == 'POST':
+            body = self.fixtures.load('b2_delete_file_version.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _b2api_v1_b2_get_upload_url(self, method, url, body, headers):
+        # test_upload_object
+        if method == 'GET':
+            body = self.fixtures.load('b2_get_upload_url.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _b2api_v1_b2_upload_file_abcd_defg(self, method, url, body, headers):
+        # test_upload_object
+        if method == 'POST':
+            body = self.fixtures.load('b2_upload_file.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _b2api_v1_b2_list_file_versions(self, method, url, body, headers):
+        if method == 'GET':
+            body = self.fixtures.load('b2_list_file_versions.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _b2api_v1_b2_hide_file(self, method, url, body, headers):
+        if method == 'POST':
+            body = self.fixtures.load('b2_hide_file.json')
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+
+class BackblazeB2MockRawResponse(MockRawResponse):
+    def _file_test00001_2_txt(self, method, url, body, headers):
+        # test_download_object
+        if method == 'GET':
+            body = 'ab'
+        else:
+            raise AssertionError('Unsupported method')
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())