Libcloud now a tlp

git-svn-id: https://svn.apache.org/repos/asf/libcloud/branches/0.2.x@1127028 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..20476a2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+*.py[co]
+*.sw[po]
+test/secrets.py
+*~
+_trial_temp
+build
diff --git a/.ratignore b/.ratignore
new file mode 100644
index 0000000..da482b2
--- /dev/null
+++ b/.ratignore
@@ -0,0 +1,4 @@
+MANIFEST.in
+.gitignore
+apidocs/
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..8935287
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include LICENSE
+include NOTICE
+include example.py
+include CONTRIBUTORS
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..a5a92a1
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,9 @@
+libcloud
+
+Copyright (c) 2009 The Apache Software Foundation.
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software developed by 
+Cloudkick (http://www.cloudkick.com/).
diff --git a/README b/README
new file mode 100644
index 0000000..13ee4d2
--- /dev/null
+++ b/README
@@ -0,0 +1,7 @@
+Apache libcloud - a unified interface into the cloud
+
+The goal of this project is to create a basic yet functional standard library 
+into various cloud providers.
+
+Apache libcloud is an incubator project at the Apache Software Foundation, see
+<http://incubator.apache.org/libcloud> for more information.
\ No newline at end of file
diff --git a/example.py b/example.py
new file mode 100644
index 0000000..d501a59
--- /dev/null
+++ b/example.py
@@ -0,0 +1,36 @@
+# 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.
+# libcloud.org 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.
+from libcloud.types import Provider
+from libcloud.providers import get_driver
+
+EC2 = get_driver(Provider.EC2_US_EAST)
+Slicehost = get_driver(Provider.SLICEHOST)
+Rackspace = get_driver(Provider.RACKSPACE)
+
+drivers = [ EC2('access key id', 'secret key'), 
+            Slicehost('api key'), 
+            Rackspace('username', 'api key') ]
+
+nodes = [ driver.list_nodes() for driver in drivers ]
+
+print nodes
+# [ <Node: provider=Amazon, status=RUNNING, name=bob, ip=1.2.3.4.5>,
+# <Node: provider=Slicehost, status=REBOOT, name=korine, ip=6.7.8.9.10>, ... ]
+
+# grab the node named "test"
+node = filter(lambda x: x.name == 'test', nodes)[0]
+
+# reboot "test"
+node.reboot()
diff --git a/libcloud/__init__.py b/libcloud/__init__.py
new file mode 100644
index 0000000..72d8f54
--- /dev/null
+++ b/libcloud/__init__.py
@@ -0,0 +1,48 @@
+# 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.
+# libcloud.org 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.
+
+"""
+libcloud provides a unified interface to the cloud computing resources.
+
+@var __version__: Current version of libcloud
+"""
+
+__all__ = ["__version__", "enable_debug"]
+
+__version__ = "0.2.1-dev"
+
+
+def enable_debug(fo):
+    """
+    Enable library wide debugging to a file-like object.
+
+    @param fo: Where to append debugging information
+    @type fo: File like object, only write operations are used.
+    """
+    import httplib
+    from libcloud.base import ConnectionKey,LoggingHTTPSConnection
+    LoggingHTTPSConnection.log = fo
+    ConnectionKey.conn_classes = (httplib.HTTPConnection, LoggingHTTPSConnection)
+
+def _init_once():
+    import os
+    d = os.getenv("LIBCLOUD_DEBUG")
+    if d:
+        if d.isdigit():
+            d = "/tmp/libcloud_debug.log"
+        fo = open(d, "a")
+        enable_debug(fo)
+
+_init_once()
diff --git a/libcloud/base.py b/libcloud/base.py
new file mode 100644
index 0000000..473617c
--- /dev/null
+++ b/libcloud/base.py
@@ -0,0 +1,546 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Provides base classes for working with drivers
+"""
+import httplib, urllib
+import libcloud
+from zope import interface
+from libcloud.interface import IConnectionUserAndKey, IResponse
+from libcloud.interface import IConnectionKey, IConnectionKeyFactory
+from libcloud.interface import IConnectionUserAndKeyFactory, IResponseFactory
+from libcloud.interface import INodeDriverFactory, INodeDriver
+from libcloud.interface import INodeFactory, INode
+from libcloud.interface import INodeSizeFactory, INodeSize
+from libcloud.interface import INodeImageFactory, INodeImage
+import hashlib
+import StringIO
+from pipes import quote as pquote
+
+class Node(object):
+    """
+    A Base Node class to derive from.
+    """
+    
+    interface.implements(INode)
+    interface.classProvides(INodeFactory)
+
+    def __init__(self, id, name, state, public_ip, private_ip,
+                 driver, extra=None):
+        self.id = id
+        self.name = name
+        self.state = state
+        self.public_ip = public_ip
+        self.private_ip = private_ip
+        self.driver = driver
+        self.uuid = self.get_uuid()
+        if not extra:
+            self.extra = {}
+        else:
+            self.extra = extra
+        
+    def get_uuid(self):
+        return hashlib.sha1("%s:%d" % (self.id,self.driver.type)).hexdigest()
+        
+    def reboot(self):
+        return self.driver.reboot_node(self)
+
+    def destroy(self):
+        return self.driver.destroy_node(self)
+
+    def __repr__(self):
+        return (('<Node: uuid=%s, name=%s, state=%s, public_ip=%s, '
+                 'provider=%s ...>')
+                % (self.uuid, self.name, self.state, self.public_ip,
+                   self.driver.name))
+
+
+class NodeSize(object):
+    """
+    A Base NodeSize class to derive from.
+    """
+    
+    interface.implements(INodeSize)
+    interface.classProvides(INodeSizeFactory)
+
+    def __init__(self, id, name, ram, disk, bandwidth, price, driver):
+        self.id = id
+        self.name = name
+        self.ram = ram
+        self.disk = disk
+        self.bandwidth = bandwidth
+        self.price = price
+        self.driver = driver
+    def __repr__(self):
+        return (('<NodeSize: id=%s, name=%s, ram=%s disk=%s bandwidth=%s '
+                 'price=%s driver=%s ...>')
+                % (self.id, self.name, self.ram, self.disk, self.bandwidth,
+                   self.price, self.driver.name))
+
+
+class NodeImage(object):
+    """
+    A Base NodeImage class to derive from.
+    """
+    
+    interface.implements(INodeImage)
+    interface.classProvides(INodeImageFactory)
+
+    def __init__(self, id, name, driver, extra=None):
+        self.id = id
+        self.name = name
+        self.driver = driver
+        if not extra:
+            self.extra = {}
+        else:
+            self.extra = extra
+    def __repr__(self):
+        return (('<NodeImage: id=%s, name=%s, driver=%s  ...>')
+                % (self.id, self.name, self.driver.name))
+
+class NodeLocation(object):
+    """
+    A base NodeLocation class to derive from.
+    """
+    interface.implements(INodeImage)
+    interface.classProvides(INodeImageFactory)
+    def __init__(self, id, name, country, driver):
+        self.id = id
+        self.name = name
+        self.country = country
+        self.driver = driver
+    def __repr__(self):
+        return (('<NodeLocation: id=%s, name=%s, country=%s, driver=%s>')
+                % (self.id, self.name, self.country, self.driver.name))
+
+class NodeAuthSSHKey(object):
+    def __init__(self, pubkey):
+        self.pubkey = pubkey
+    def __repr__(self):
+        return '<NodeAuthSSHKey>'
+
+class NodeAuthPassword(object):
+    def __init__(self, password):
+        self.password = password
+    def __repr__(self):
+        return '<NodeAuthPassword>'
+
+class Response(object):
+    """
+    A Base Response class to derive from.
+    """
+    interface.implements(IResponse)
+    interface.classProvides(IResponseFactory)
+
+    NODE_STATE_MAP = {}
+
+    object = None
+    body = None
+    status = httplib.OK
+    headers = {}
+    error = None
+    connection = None
+
+    def __init__(self, response):
+        self.body = response.read()
+        self.status = response.status
+        self.headers = dict(response.getheaders())
+        self.error = response.reason
+
+        if not self.success():
+            raise Exception(self.parse_error())
+
+        self.object = self.parse_body()
+
+    def parse_body(self):
+        """
+        Parse response body.
+
+        Override in a provider's subclass.
+
+        @return: Parsed body.
+        """
+        return self.body
+
+    def parse_error(self):
+        """
+        Parse the error messages.
+
+        Override in a provider's subclass.
+
+        @return: Parsed error.
+        """
+        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?
+
+        @return: C{True} or C{False}
+        """
+        return self.status == httplib.OK or self.status == httplib.CREATED
+
+#TODO: Move this to a better location/package
+class LoggingHTTPSConnection(httplib.HTTPSConnection):
+    """
+    Debug class to log all HTTP(s) requests as they could be made
+    with the C{curl} command.
+
+    @cvar log: file-like object that logs entries are written to.
+    """
+    log = None
+
+    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:
+            def __init__(self, s):
+                self.s = s
+            def makefile(self, mode, foo):
+                return StringIO.StringIO(self.s)
+        rr = r
+        if r.chunked:
+            ht += "%x\r\n" % (len(body))
+            ht += body
+            ht += "\r\n0\r\n"
+        else:
+            ht += body
+        rr = httplib.HTTPResponse(fakesock(ht),
+                                  method=r._method,
+                                  debuglevel=r.debuglevel)
+        rr.begin()
+        rv += ht
+        rv += ("\n# -------- end %d:%d response ----------\n"
+               % (id(self), id(r)))
+        return (rr, rv)
+
+    def getresponse(self):
+        r = httplib.HTTPSConnection.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 _log_curl(self, method, url, body, headers):
+        cmd = ["curl", "-i"]
+
+        cmd.extend(["-X", pquote(method)])
+
+        for h in headers:
+          cmd.extend(["-H", pquote("%s: %s" % (h, headers[h]))])
+
+        # 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([pquote("https://%s:%d%s" % (self.host, self.port, url))])
+        return " ".join(cmd)
+
+    def request(self, method, url, body=None, headers=None):
+        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 httplib.HTTPSConnection.request(self, method, url,
+                                               body, headers)
+
+class ConnectionKey(object):
+    """
+    A Base Connection class to derive from.
+    """
+    interface.implementsOnly(IConnectionKey)
+    interface.classProvides(IConnectionKeyFactory)
+
+    #conn_classes = (httplib.HTTPConnection, LoggingHTTPSConnection)
+    conn_classes = (httplib.HTTPConnection, httplib.HTTPSConnection)
+
+    responseCls = Response
+    connection = None
+    host = '127.0.0.1'
+    port = (80, 443)
+    secure = 1
+    driver = None
+
+    def __init__(self, key, secure=True):
+        """
+        Initialize `user_id` and `key`; set `secure` to an C{int} based on
+        passed value.
+        """
+        self.key = key
+        self.secure = secure and 1 or 0
+        self.ua = []
+
+    def connect(self, host=None, port=None):
+        """
+        Establish a connection with the API server.
+
+        @type host: C{str}
+        @param host: Optional host to override our default
+
+        @type port: C{int}
+        @param port: Optional port to override our default
+
+        @returns: A connection
+        """
+        host = host or self.host
+        port = port or self.port[self.secure]
+
+        connection = self.conn_classes[self.secure](host, port)
+        self.connection = connection
+
+    def _user_agent(self):
+      return 'libcloud/%s (%s)%s' % (
+                libcloud.__version__,
+                self.driver.name,
+                "".join([" (%s)" % x for x in self.ua]))
+
+    def user_agent_append(self, s):
+      self.ua.append(s)
+
+    def request(self,
+                action,
+                params=None,
+                data='',
+                headers=None,
+                method='GET'):
+        """
+        Request a given `action`.
+        
+        Basically a wrapper around the connection
+        object's `request` that does some helpful pre-processing.
+
+        @type action: C{str}
+        @param action: A path
+
+        @type params: C{dict}
+        @param params: Optional mapping of additional parameters to send. If
+            None, leave as an empty C{dict}.
+
+        @type data: C{unicode}
+        @param data: A body of data to send with the request.
+
+        @type headers: C{dict}
+        @param headers: Extra headers to add to the request
+            None, leave as an empty C{dict}.
+
+        @type method: C{str}
+        @param method: An HTTP method such as "GET" or "POST".
+
+        @return: An instance of type I{responseCls}
+        """
+        if params is None:
+          params = {}
+        if headers is None:
+          headers = {}
+        # Extend default parameters
+        params = self.add_default_params(params)
+        # Extend default headers
+        headers = self.add_default_headers(headers)
+        # We always send a content length and user-agent header
+        headers.update({'Content-Length': len(data)})
+        headers.update({'User-Agent': self._user_agent()})
+        headers.update({'Host': self.host})
+        # Encode data if necessary
+        if data != '':
+            data = self.encode_data(data)
+        url = '?'.join((action, urllib.urlencode(params)))
+        
+        # Removed terrible hack...this a less-bad hack that doesn't execute a
+        # request twice, but it's still a hack.
+        self.connect()
+        self.connection.request(method=method, url=url, body=data,
+                                headers=headers)
+        response = self.responseCls(self.connection.getresponse())
+        response.connection = self
+        return response
+
+    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 encode_data(self, data):
+        """
+        Encode body data.
+
+        Override in a provider's subclass.
+        """
+        return data
+
+
+class ConnectionUserAndKey(ConnectionKey):
+    """
+    Base connection which accepts a user_id and key
+    """
+    interface.implementsOnly(IConnectionUserAndKey)
+    interface.classProvides(IConnectionUserAndKeyFactory)
+
+    user_id = None
+
+    def __init__(self, user_id, key, secure=True):
+        super(ConnectionUserAndKey, self).__init__(key, secure)
+        self.user_id = user_id
+
+
+class NodeDriver(object):
+    """
+    A base NodeDriver class to derive from
+    """
+    interface.implements(INodeDriver)
+    interface.classProvides(INodeDriverFactory)
+
+    connectionCls = ConnectionKey
+    name = None
+    type = None
+    features = {"create_node": []}
+    """
+    List of available features for a driver.
+        - L{create_node}
+            - ssh_key: Supports L{NodeAuthSSHKey} as an authentication method
+              for nodes.
+            - password: Supports L{NodeAuthPassword} as an authentication
+              method for nodes.
+    """
+    NODE_STATE_MAP = {}
+
+    def __init__(self, key, secret=None, secure=True):
+        """
+        @keyword    key:    API key or username to used
+        @type       key:    str
+
+        @keyword    secret: Secret password to be used
+        @type       secret: str
+
+        @keyword    secure: Weither to use HTTPS or HTTP. Note: Some providers 
+                            only support HTTPS, and it is on by default.
+        @type       secure: bool
+        """
+        self.key = key
+        self.secret = secret
+        self.secure = secure
+        if self.secret:
+          self.connection = self.connectionCls(key, secret, secure)
+        else:
+          self.connection = self.connectionCls(key, secure)
+
+        self.connection.driver = self
+        self.connection.connect()
+
+    def create_node(self, **kwargs):
+        """Create a new node instance.
+
+        @keyword    name:   String with a name for this new node (required)
+        @type       name:   str
+
+        @keyword    size:   The size of resources allocated to this node.
+                            (required)
+        @type       size:   L{NodeSize}
+
+        @keyword    image:  OS Image to boot on node. (required)
+        @type       image:  L{NodeImage}
+
+        @keyword    location: Which data center to create a node in. If empty,
+                              undefined behavoir will be selected. (optional)
+        @type       location: L{NodeLocation}
+
+        @keyword    auth:   Initial authentication information for the node
+                            (optional)
+        @type       auth:   L{NodeAuthSSHKey} or L{NodeAuthPassword}
+
+        @return: The newly created L{Node}.
+        """
+        raise NotImplementedError, \
+            'create_node not implemented for this driver'
+
+    def destroy_node(self, node):
+        """Destroy a node.
+
+        Depending upon the provider, this may destroy all data associated with
+        the node, including backups.
+
+        @return: C{bool} True if the destroy was successful, otherwise False
+        """
+        raise NotImplementedError, \
+            'destroy_node not implemented for this driver'
+
+    def reboot_node(self, node):
+        """
+        Reboot a node.
+        @return: C{bool} True if the reboot was successful, otherwise False
+        """
+        raise NotImplementedError, \
+            'reboot_node not implemented for this driver'
+
+    def list_nodes(self):
+        """
+        List all nodes
+        @return: C{list} of L{Node} objects
+        """
+        raise NotImplementedError, \
+            'list_nodes not implemented for this driver'
+
+    def list_images(self, location=None):
+        """
+        List images on a provider
+        @return: C{list} of L{NodeImage} objects
+        """
+        raise NotImplementedError, \
+            'list_images not implemented for this driver'
+
+    def list_sizes(self, location=None):
+        """
+        List sizes on a provider
+        @return: C{list} of L{NodeSize} objects
+        """
+        raise NotImplementedError, \
+            'list_sizes not implemented for this driver'
+
+    def list_locations(self):
+        """
+        List data centers for a provider
+        @return: C{list} of L{NodeLocation} objects
+        """
+        raise NotImplementedError, \
+            'list_locations not implemented for this driver'
diff --git a/libcloud/drivers/__init__.py b/libcloud/drivers/__init__.py
new file mode 100644
index 0000000..8f7f612
--- /dev/null
+++ b/libcloud/drivers/__init__.py
@@ -0,0 +1,18 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Drivers for working with different providers
+"""
diff --git a/libcloud/drivers/dummy.py b/libcloud/drivers/dummy.py
new file mode 100644
index 0000000..517b6c8
--- /dev/null
+++ b/libcloud/drivers/dummy.py
@@ -0,0 +1,140 @@
+# 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.
+# libcloud.org 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.
+"""
+Dummy Driver
+
+@note: This driver is out of date
+"""
+from libcloud.interface import INodeDriver
+from libcloud.base import ConnectionKey, NodeDriver, NodeSize, NodeLocation
+from libcloud.base import NodeImage, Node
+from libcloud.types import Provider,NodeState
+from zope.interface import implements
+
+import uuid
+
+class DummyConnection(ConnectionKey):
+
+  def connect(self, host=None, port=None):
+    pass
+
+class DummyNodeDriver(NodeDriver):
+
+    name = "Dummy Node Provider"
+    type = Provider.DUMMY
+
+    implements(INodeDriver)
+
+    def __init__(self, creds):
+        self.creds = creds
+        self.nl = [
+            Node(id=1,
+                 name='dummy-1',
+                 state=NodeState.RUNNING,
+                 public_ip=['127.0.0.1'],
+                 private_ip=[],
+                 driver=self,
+                 extra={'foo': 'bar'}),
+            Node(id=2,
+                 name='dummy-2',
+                 state=NodeState.RUNNING,
+                 public_ip=['127.0.0.1'],
+                 private_ip=[],
+                 driver=self,
+                 extra={'foo': 'bar'}),
+        ]
+        self.connection = DummyConnection(self.creds)
+
+    def get_uuid(self, unique_field=None):
+        return str(uuid.uuid4())
+
+    def list_nodes(self):
+        return self.nl
+
+    def reboot_node(self, node):
+        node.state = NodeState.REBOOTING
+        return True
+
+    def destroy_node(self, node):
+        node.state = NodeState.TERMINATED
+        self.nl.remove(node)
+        return True
+
+    def list_images(self, location=None):
+        return [
+            NodeImage(id=1, name="Ubuntu 9.10", driver=self),
+            NodeImage(id=2, name="Ubuntu 9.04", driver=self),
+            NodeImage(id=3, name="Slackware 4", driver=self),
+        ]
+
+    def list_sizes(self, location=None):
+        return [
+          NodeSize(id=1,
+                   name="Small",
+                   ram=128,
+                   disk=4,
+                   bandwidth=500,
+                   price=4,
+                   driver=self),
+          NodeSize(id=2,
+                   name="Medium",
+                   ram=512,
+                   disk=16,
+                   bandwidth=1500,
+                   price=8,
+                   driver=self),
+          NodeSize(id=3,
+                   name="Big",
+                   ram=4096,
+                   disk=32,
+                   bandwidth=2500,
+                   price=32,
+                   driver=self),
+          NodeSize(id=4,
+                   name="XXL Big",
+                   ram=4096*2,
+                   disk=32*4,
+                   bandwidth=2500*3,
+                   price=32*2,
+                   driver=self),
+        ]
+
+    def list_locations(self):
+        return [
+          NodeLocation(id=1,
+                       name="Paul's Room",
+                       country='US',
+                       driver=self),
+          NodeLocation(id=1,
+                       name="London Loft",
+                       country='GB',
+                       driver=self),
+          NodeLocation(id=1,
+                       name="Island Datacenter",
+                       country='FJ',
+                       driver=self),
+        ]
+
+    def create_node(self, **kwargs):
+        l = len(self.nl) + 1
+        n = Node(id=l,
+                 name='dummy-%d' % l,
+                 state=NodeState.RUNNING,
+                 public_ip=['127.0.0.%d' % l],
+                 private_ip=[],
+                 driver=self,
+                 extra={'foo': 'bar'})
+        self.nl.append(n)
+        return n
diff --git a/libcloud/drivers/ec2.py b/libcloud/drivers/ec2.py
new file mode 100644
index 0000000..228cac9
--- /dev/null
+++ b/libcloud/drivers/ec2.py
@@ -0,0 +1,434 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Amazon EC2 driver
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, Response, ConnectionUserAndKey
+from libcloud.base import NodeDriver, NodeSize, NodeImage, NodeLocation
+import base64
+import hmac
+from hashlib import sha256
+import time
+import urllib
+from xml.etree import ElementTree as ET
+
+EC2_US_EAST_HOST = 'ec2.us-east-1.amazonaws.com'
+EC2_US_WEST_HOST = 'ec2.us-west-1.amazonaws.com'
+EC2_EU_WEST_HOST = 'ec2.eu-west-1.amazonaws.com'
+
+API_VERSION = '2009-04-04'
+NAMESPACE = "http://ec2.amazonaws.com/doc/%s/" % (API_VERSION)
+
+"""
+Sizes must be hardcoded, because Amazon doesn't provide an API to fetch them.
+From http://aws.amazon.com/ec2/instance-types/
+"""
+EC2_INSTANCE_TYPES = {
+    'm1.small': {
+        'id': 'm1.small',
+        'name': 'Small Instance',
+        'ram': 1740,
+        'disk': 160,
+        'bandwidth': None
+    },
+    'm1.large': {
+        'id': 'm1.large',
+        'name': 'Large Instance',
+        'ram': 7680,
+        'disk': 850,
+        'bandwidth': None
+    },
+    'm1.xlarge': {
+        'id': 'm1.xlarge',
+        'name': 'Extra Large Instance',
+        'ram': 15360,
+        'disk': 1690,
+        'bandwidth': None
+    },
+    'c1.medium': {
+        'id': 'c1.medium',
+        'name': 'High-CPU Medium Instance',
+        'ram': 1740,
+        'disk': 350,
+        'bandwidth': None
+    },
+    'c1.xlarge': {
+        'id': 'c1.xlarge',
+        'name': 'High-CPU Extra Large Instance',
+        'ram': 7680,
+        'disk': 1690,
+        'bandwidth': None
+    },
+    'm2.2xlarge': {
+        'id': 'm2.2xlarge',
+        'name': 'High-Memory Double Extra Large Instance',
+        'ram': 35021,
+        'disk': 850,
+        'bandwidth': None
+    },
+    'm2.4xlarge': {
+        'id': 'm2.4xlarge',
+        'name': 'High-Memory Quadruple Extra Large Instance',
+        'ram': 70042,
+        'disk': 1690,
+        'bandwidth': None
+    },
+}
+
+EC2_US_EAST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+EC2_US_WEST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+EC2_EU_WEST_INSTANCE_TYPES = dict(EC2_INSTANCE_TYPES)
+
+EC2_US_EAST_INSTANCE_TYPES['m1.small']['price'] = '.085'
+EC2_US_EAST_INSTANCE_TYPES['m1.large']['price'] = '.34'
+EC2_US_EAST_INSTANCE_TYPES['m1.xlarge']['price'] = '.68'
+EC2_US_EAST_INSTANCE_TYPES['c1.medium']['price'] = '.17'
+EC2_US_EAST_INSTANCE_TYPES['c1.xlarge']['price'] = '.68'
+EC2_US_EAST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.2'
+EC2_US_EAST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.4'
+
+EC2_US_WEST_INSTANCE_TYPES['m1.small']['price'] = '.095'
+EC2_US_WEST_INSTANCE_TYPES['m1.large']['price'] = '.38'
+EC2_US_WEST_INSTANCE_TYPES['m1.xlarge']['price'] = '.76'
+EC2_US_WEST_INSTANCE_TYPES['c1.medium']['price'] = '.19'
+EC2_US_WEST_INSTANCE_TYPES['c1.xlarge']['price'] = '.76'
+EC2_US_WEST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.34'
+EC2_US_WEST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.68'
+
+EC2_EU_WEST_INSTANCE_TYPES['m1.small']['price'] = '.095'
+EC2_EU_WEST_INSTANCE_TYPES['m1.large']['price'] = '.38'
+EC2_EU_WEST_INSTANCE_TYPES['m1.xlarge']['price'] = '.76'
+EC2_EU_WEST_INSTANCE_TYPES['c1.medium']['price'] = '.19'
+EC2_EU_WEST_INSTANCE_TYPES['c1.xlarge']['price'] = '.76'
+EC2_EU_WEST_INSTANCE_TYPES['m2.2xlarge']['price'] = '1.34'
+EC2_EU_WEST_INSTANCE_TYPES['m2.4xlarge']['price'] = '2.68'
+
+class EC2Response(Response):
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        return ET.XML(self.body)
+
+    def parse_error(self):
+        err_list = []
+        for err in ET.XML(self.body).findall('Errors/Error'):
+            code, message = err.getchildren()
+            err_list.append("%s: %s" % (code.text, message.text))
+            if code.text == "InvalidClientTokenId":
+                raise InvalidCredsException(err_list[-1])
+            if code.text == "SignatureDoesNotMatch":
+                raise InvalidCredsException(err_list[-1])
+        return "\n".join(err_list)
+
+class EC2Connection(ConnectionUserAndKey):
+
+    host = EC2_US_EAST_HOST
+    responseCls = EC2Response
+
+    def add_default_params(self, params):
+        params['SignatureVersion'] = '2'
+        params['SignatureMethod'] = 'HmacSHA256'
+        params['AWSAccessKeyId'] = self.user_id
+        params['Version'] = API_VERSION
+        params['Timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', 
+                                            time.gmtime())
+        params['Signature'] = self._get_aws_auth_param(params, self.key)
+        return params
+        
+    def _get_aws_auth_param(self, params, secret_key, path='/'):
+        """
+        Creates the signature required for AWS, per
+        http://bit.ly/aR7GaQ [docs.amazonwebservices.com]:
+
+        StringToSign = HTTPVerb + "\n" +
+                       ValueOfHostHeaderInLowercase + "\n" +
+                       HTTPRequestURI + "\n" +
+                       CanonicalizedQueryString <from the preceding step>
+        """
+        keys = params.keys()
+        keys.sort()
+        pairs = []
+        for key in keys:
+            pairs.append(urllib.quote(key, safe='') + '=' +
+                         urllib.quote(params[key], safe='-_~'))
+
+        qs = '&'.join(pairs)
+        string_to_sign = '\n'.join(('GET', self.host, path, qs))
+                                         
+        b64_hmac = base64.b64encode(
+            hmac.new(secret_key, string_to_sign, digestmod=sha256).digest()
+        )
+        return b64_hmac
+
+class EC2NodeDriver(NodeDriver):
+
+    connectionCls = EC2Connection
+    type = Provider.EC2
+    name = 'Amazon EC2 (us-east-1)'
+
+    _instance_types = EC2_US_EAST_INSTANCE_TYPES
+
+    NODE_STATE_MAP = {
+        'pending': NodeState.PENDING,
+        'running': NodeState.RUNNING,
+        'shutting-down': NodeState.TERMINATED,
+        'terminated': NodeState.TERMINATED
+    }
+
+    def _findtext(self, element, xpath):
+        return element.findtext(self._fixxpath(xpath))
+
+    def _fixxpath(self, xpath):
+        # ElementTree wants namespaces in its xpaths, so here we add them.
+        return "/".join(["{%s}%s" % (NAMESPACE, e) for e in xpath.split("/")])
+
+    def _findattr(self, element, xpath):
+        return element.findtext(self._fixxpath(xpath))
+
+    def _findall(self, element, xpath):
+        return element.findall(self._fixxpath(xpath))
+
+    def _pathlist(self, key, arr):
+        """
+        Converts a key and an array of values into AWS query param format.
+        """
+        params = {}
+        i = 0
+        for value in arr:
+            i += 1
+            params["%s.%s" % (key, i)] = value
+        return params
+
+    def _get_boolean(self, element):
+        tag = "{%s}%s" % (NAMESPACE, 'return')
+        return element.findtext(tag) == 'true'
+
+    def _get_terminate_boolean(self, element):
+        status = element.findtext(".//{%s}%s" % (NAMESPACE, 'name'))
+        return any([ term_status == status
+                     for term_status
+                     in ('shutting-down', 'terminated') ])
+
+    def _to_nodes(self, object, xpath):
+        return [ self._to_node(el) 
+                 for el in object.findall(self._fixxpath(xpath)) ]
+        
+    def _to_node(self, element):
+        try:
+            state = self.NODE_STATE_MAP[
+                self._findattr(element, "instanceState/name")
+            ]
+        except KeyError:
+            state = NodeState.UNKNOWN
+
+        n = Node(
+            id=self._findtext(element, 'instanceId'),
+            name=self._findtext(element, 'instanceId'),
+            state=state,
+            public_ip=[self._findtext(element, 'dnsName')],
+            private_ip=[self._findtext(element, 'privateDnsName')],
+            driver=self.connection.driver,
+            extra={
+                'dns_name': self._findattr(element, "dnsName"),
+                'instanceId': self._findattr(element, "instanceId"),
+                'imageId': self._findattr(element, "imageId"),
+                'private_dns': self._findattr(element, "privateDnsName"),
+                'status': self._findattr(element, "instanceState/name"),
+                'keyname': self._findattr(element, "keyName"),
+                'launchindex': self._findattr(element, "amiLaunchIndex"),
+                'productcode':
+                    [p.text for p in self._findall(
+                        element, "productCodesSet/item/productCode"
+                     )],
+                'instancetype': self._findattr(element, "instanceType"),
+                'launchdatetime': self._findattr(element, "launchTime"),
+                'availability': self._findattr(element,
+                                               "placement/availabilityZone"),
+                'kernelid': self._findattr(element, "kernelId"),
+                'ramdiskid': self._findattr(element, "ramdiskId")
+            }
+        )
+        return n
+
+    def _to_images(self, object):
+        return [ self._to_image(el)
+                 for el in object.findall(
+                    self._fixxpath('imagesSet/item')
+                 ) ]
+
+    def _to_image(self, element):
+        n = NodeImage(id=self._findtext(element, 'imageId'),
+                      name=self._findtext(element, 'imageLocation'),
+                      driver=self.connection.driver)
+        return n
+
+    def list_nodes(self):
+        params = {'Action': 'DescribeInstances' }
+        nodes = self._to_nodes(
+                    self.connection.request('/', params=params).object,
+                    'reservationSet/item/instancesSet/item')
+        return nodes
+
+    def list_sizes(self, location=None):
+        return [ NodeSize(driver=self.connection.driver, **i) 
+                    for i in self._instance_types.values() ]
+    
+    def list_images(self, location=None):
+        params = {'Action': 'DescribeImages'}
+        images = self._to_images(
+            self.connection.request('/', params=params).object
+        )
+        return images
+
+    def create_security_group(self, name, description):
+        params = {'Action': 'CreateSecurityGroup',
+                  'GroupName': name,
+                  'GroupDescription': description}
+        return self.connection.request('/', params=params).object
+
+    def authorize_security_group_permissive(self, name):
+        results = []
+        params = {'Action': 'AuthorizeSecurityGroupIngress',
+                  'GroupName': name,
+                  'IpProtocol': 'tcp',
+                  'FromPort': '0',
+                  'ToPort': '65535',
+                  'CidrIp': '0.0.0.0/0'}
+        try:
+            results.append(
+                self.connection.request('/', params=params.copy()).object
+            )
+        except Exception, e:
+            if e.args[0].find("InvalidPermission.Duplicate") == -1:
+                raise e
+        params['IpProtocol'] = 'udp'
+
+        try:
+            results.append(
+                self.connection.request('/', params=params.copy()).object
+            )
+        except Exception, e:
+            if e.args[0].find("InvalidPermission.Duplicate") == -1:
+                raise e
+
+        params.update({'IpProtocol': 'icmp', 'FromPort': '-1', 'ToPort': '-1'})
+
+        try:
+            results.append(
+                self.connection.request('/', params=params.copy()).object
+            )
+        except Exception, e:
+            if e.args[0].find("InvalidPermission.Duplicate") == -1:
+                raise e
+        return results
+
+    def create_node(self, **kwargs):
+        """Create a new EC2 node
+
+        See L{NodeDriver.create_node} for more keyword args.
+        Reference: http://bit.ly/8ZyPSy [docs.amazonwebservices.com]
+
+        @keyword    name: Name (unused by EC2)
+        @type       name: C{str}
+
+        @keyword    mincount: Minimum number of instances to launch
+        @type       mincount: C{int}
+
+        @keyword    maxcount: Maximum number of instances to launch
+        @type       maxcount: C{int}
+
+        @keyword    securitygroup: Name of security group
+        @type       securitygroup: C{str}
+
+        @keyword    keyname: The name of the key pair
+        @type       keyname: C{str}
+
+        @keyword    userdata: User data
+        @type       userdata: C{str}
+        """
+        name = kwargs["name"]
+        image = kwargs["image"]
+        size = kwargs["size"]
+        params = {
+            'Action': 'RunInstances',
+            'ImageId': image.id,
+            'MinCount': kwargs.get('mincount','1'),
+            'MaxCount': kwargs.get('maxcount','1'),
+            'InstanceType': size.id
+        }
+
+        if 'securitygroup' in kwargs:
+            params['SecurityGroup'] = kwargs['securitygroup']
+
+        if 'keyname' in kwargs:
+            params['KeyName'] = kwargs['keyname']
+
+        if 'userdata' in kwargs:
+            params['UserData'] = base64.b64encode(kwargs['userdata'])
+
+        object = self.connection.request('/', params=params).object
+        nodes = self._to_nodes(object, 'instancesSet/item')
+
+        if len(nodes) == 1:
+            return nodes[0]
+        else:
+            return nodes
+
+    def reboot_node(self, node):
+        """
+        Reboot the node by passing in the node object
+        """
+        params = {'Action': 'RebootInstances'}
+        params.update(self._pathlist('InstanceId', [node.id]))
+        res = self.connection.request('/', params=params).object
+        return self._get_boolean(res)
+
+    def destroy_node(self, node):
+        """
+        Destroy node by passing in the node object
+        """
+        params = {'Action': 'TerminateInstances'}
+        params.update(self._pathlist('InstanceId', [node.id]))
+        res = self.connection.request('/', params=params).object
+        return self._get_terminate_boolean(res)
+
+    def list_locations(self):
+        return [NodeLocation(0, 'Amazon US N. Virginia', 'US', self)]
+
+class EC2EUConnection(EC2Connection):
+
+    host = EC2_EU_WEST_HOST
+
+class EC2EUNodeDriver(EC2NodeDriver):
+
+    connectionCls = EC2EUConnection
+    _instance_types = EC2_EU_WEST_INSTANCE_TYPES
+    def list_locations(self):
+        return [NodeLocation(0, 'Amazon Europe Ireland', 'IE', self)]
+
+class EC2USWestConnection(EC2Connection):
+
+    host = EC2_US_WEST_HOST
+
+class EC2USWestNodeDriver(EC2NodeDriver):
+
+    connectionCls = EC2USWestConnection
+    _instance_types = EC2_US_WEST_INSTANCE_TYPES
+    def list_locations(self):
+        return [NodeLocation(0, 'Amazon US N. California', 'US', self)]
diff --git a/libcloud/drivers/gogrid.py b/libcloud/drivers/gogrid.py
new file mode 100644
index 0000000..264951d
--- /dev/null
+++ b/libcloud/drivers/gogrid.py
@@ -0,0 +1,234 @@
+# 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.
+# libcloud.org 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.
+"""
+GoGrid driver
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, ConnectionUserAndKey, Response, NodeDriver
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+import time
+import hashlib
+
+# JSON is included in the standard library starting with Python 2.6.  For 2.5
+# and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson
+try: import json
+except: import simplejson as json
+
+HOST = 'api.gogrid.com'
+PORTS_BY_SECURITY = { True: 443, False: 80 }
+API_VERSION = '1.3'
+
+STATE = {
+    "Starting": NodeState.PENDING,
+    "On": NodeState.RUNNING,
+    "Off": NodeState.PENDING,
+    "Restarting": NodeState.REBOOTING,
+    "Saving": NodeState.PENDING,
+    "Restoring": NodeState.PENDING,
+}
+
+GOGRID_INSTANCE_TYPES = {'512MB': {'id': '512MB',
+                       'name': '512MB',
+                       'ram': 512,
+                       'disk': 30,
+                       'bandwidth': None,
+                       'price':0.095},
+        '1GB': {'id': '1GB',
+                       'name': '1GB',
+                       'ram': 1024,
+                       'disk': 60,
+                       'bandwidth': None,
+                       'price':0.19},
+        '2GB': {'id': '2GB',
+                       'name': '2GB',
+                       'ram': 2048,
+                       'disk': 120,
+                       'bandwidth': None,
+                       'price':0.38},
+        '4GB': {'id': '4GB',
+                       'name': '4GB',
+                       'ram': 4096,
+                       'disk': 240,
+                       'bandwidth': None,
+                       'price':0.76},
+        '8GB': {'id': '8GB',
+                       'name': '8GB',
+                       'ram': 8192,
+                       'disk': 480,
+                       'bandwidth': None,
+                       'price':1.52}}
+
+
+class GoGridResponse(Response):
+    def success(self):
+        if self.status == 403:
+          raise InvalidCredsException()
+        if not self.body:
+            return None
+        return json.loads(self.body)['status'] == 'success'
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        return json.loads(self.body)
+
+    def parse_error(self):
+        if not self.object:
+            return None
+        return self.object['message']
+
+class GoGridConnection(ConnectionUserAndKey):
+
+    host = HOST
+    responseCls = GoGridResponse
+
+    def add_default_params(self, params):
+        params["api_key"] = self.user_id
+        params["v"] = API_VERSION
+        params["format"] = 'json'
+        params["sig"] = self.get_signature(self.user_id, self.key)
+
+        return params
+        
+    def get_signature(self, key, secret):
+        """ create sig from md5 of key + secret + time """
+        m = hashlib.md5(key+secret+str(int(time.time())))
+        return m.hexdigest()
+
+class GoGridNode(Node):
+    # Generating uuid based on public ip to get around missing id on
+    # create_node in gogrid api
+    #
+    # Used public ip since it is not mutable and specified at create time,
+    # so uuid of node should not change after add is completed
+    def get_uuid(self):
+        return hashlib.sha1(
+            "%s:%d" % (self.public_ip,self.driver.type)
+        ).hexdigest()
+
+class GoGridNodeDriver(NodeDriver):
+
+    connectionCls = GoGridConnection
+    type = Provider.GOGRID
+    name = 'GoGrid API'
+
+    _instance_types = GOGRID_INSTANCE_TYPES
+
+    def get_state(self, element):
+        try:
+            return STATE[element['state']['name']]
+        except:
+            pass
+        return NodeState.UNKNOWN
+
+    def get_ip(self, element):
+        return element['ip']['ip']
+
+    def get_id(self,element):
+        return element['id']
+
+    def _to_node(self, element):
+        state = self.get_state(element)
+        ip = self.get_ip(element)
+        id = self.get_id(element)
+        n = GoGridNode(id=id,
+                 name=element['name'],
+                 state=state,
+                 public_ip=[ip],
+                 private_ip=[],
+                 driver=self.connection.driver)
+        return n
+
+    def _to_image(self, element):
+        n = NodeImage(id=element['id'],
+                      name=element['friendlyName'],
+                      driver=self.connection.driver)
+        return n
+
+    def _to_images(self, object):
+        return [ self._to_image(el)
+                 for el in object['list'] ]
+
+    def list_images(self, location=None):
+        images = self._to_images(
+                    self.connection.request('/api/grid/image/list').object)
+        return images
+
+    def get_uuid(self, field):
+        uuid_str = "%s:%s" % (field,self.connection.user_id)
+        return hashlib.sha1(uuid_str).hexdigest()
+
+    def list_nodes(self):
+        res = self.server_list()
+        return [ self._to_node(el)
+                 for el
+                 in res['list'] ]
+
+    def reboot_node(self, node):
+        id = node.id
+        power = 'restart'
+        res = self.server_power(id, power)
+        if not res.success():
+            raise Exception(res.parse_error())
+        return True
+
+    def destroy_node(self, node):
+        id = node.id
+        res = self.server_delete(id)
+        if not res.success():
+            raise Exception(res.parse_error())
+        return True
+
+    def server_list(self):
+        return self.connection.request('/api/grid/server/list').object
+
+    def server_power(self, id, power):
+        # power in ['start', 'stop', 'restart']
+        params = {'id': id, 'power': power}
+        return self.connection.request("/api/grid/server/power", params)
+
+    def server_delete(self, id):
+        params = {'id': id}
+        return self.connection.request("/api/grid/server/delete", params)
+
+    def get_first_ip(self):
+        params = {'ip.state': 'Unassigned', 'ip.type':'public'}
+        object = self.connection.request("/api/grid/ip/list", params).object
+        return object['list'][0]['ip']
+
+    def list_sizes(self, location=None):
+        return [ NodeSize(driver=self.connection.driver, **i) 
+                    for i in self._instance_types.values() ]
+
+    def list_locations(self):
+        return [NodeLocation(0, "GoGrid Los Angeles", 'US', self)]
+
+    def create_node(self, **kwargs):
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+        first_ip = self.get_first_ip()
+        params = {'name': name,
+                  'image': image.id,
+                  'description': kwargs.get('description',''),
+                  'server.ram': size.id,
+                  'ip':first_ip}
+
+        object = self.connection.request('/api/grid/server/add',
+                                         params=params).object
+        node = self._to_node(object['list'][0])
+
+        return node
diff --git a/libcloud/drivers/linode.py b/libcloud/drivers/linode.py
new file mode 100644
index 0000000..f5d5c33
--- /dev/null
+++ b/libcloud/drivers/linode.py
@@ -0,0 +1,439 @@
+# 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.
+# libcloud.org 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.
+#
+# Maintainer: Jed Smith <jsmith@linode.com>
+#
+# BETA TESTING THE LINODE API AND DRIVERS
+#
+# A beta account that incurs no financial charge may be arranged for.  Please
+# contact Jed Smith <jsmith@linode.com> for your request.
+#
+"""
+Linode driver
+"""
+from libcloud.types import Provider, NodeState, InvalidCredsException
+from libcloud.base import ConnectionKey, Response
+from libcloud.base import NodeDriver, NodeSize, Node, NodeLocation
+from libcloud.base import NodeAuthPassword, NodeAuthSSHKey
+from libcloud.base import NodeImage
+from copy import copy
+import os
+
+# JSON is included in the standard library starting with Python 2.6.  For 2.5
+# and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson
+try: import json
+except: import simplejson as json
+
+
+# Base exception for problems arising from this driver
+class LinodeException(Exception):
+    def __str__(self):
+        return "(%u) %s" % (self.args[0], self.args[1])
+    def __repr__(self):
+        return "<LinodeException code %u '%s'>" % (self.args[0], self.args[1])
+
+# For beta accounts, change this to "beta.linode.com".
+LINODE_API = "api.linode.com"
+
+# For beta accounts, change this to "/api/".
+LINODE_ROOT = "/"
+
+
+class LinodeResponse(Response):
+    # Wraps a Linode API HTTP response.
+    
+    def __init__(self, response):
+        # Given a response object, slurp the information from it.
+        self.body = response.read()
+        self.status = response.status
+        self.headers = dict(response.getheaders())
+        self.error = response.reason
+        self.invalid = LinodeException(0xFF,
+                                       "Invalid JSON received from server")
+        
+        # Move parse_body() to here;  we can't be sure of failure until we've
+        # parsed the body into JSON.
+        self.action, self.object, self.errors = self.parse_body()
+        
+        if self.error == "Moved Temporarily":
+            raise LinodeException(0xFA,
+                                  "Redirected to error page by API.  Bug?")
+
+        if not self.success():
+            # Raise the first error, as there will usually only be one
+            raise self.errors[0]
+    
+    def parse_body(self):
+        # Parse the body of the response into JSON.  Will return None if the
+        # JSON response chokes the parser.  Returns a triple:
+        #    (action, data, errorarray)
+        try:
+            js = json.loads(self.body)
+            if ("DATA" not in js
+                or "ERRORARRAY" not in js
+                or "ACTION" not in js):
+
+                return (None, None, [self.invalid])
+            errs = [self._make_excp(e) for e in js["ERRORARRAY"]]
+            return (js["ACTION"], js["DATA"], errs)
+        except:
+            # Assume invalid JSON, and use an error code unused by Linode API.
+            return (None, None, [self.invalid])
+    
+    def parse_error(self):
+        # Obtain the errors from the response.  Will always return a list.
+        try:
+            js = json.loads(self.body)
+            if "ERRORARRAY" not in js:
+                return [self.invalid]
+            return [self._make_excp(e) for e in js["ERRORARRAY"]]
+        except:
+            return [self.invalid]
+    
+    def success(self):
+        # Does the response indicate success?  If ERRORARRAY has more than one
+        # entry, we'll say no.
+        return len(self.errors) == 0
+    
+    def _make_excp(self, error):
+        # Make an exception from an entry in ERRORARRAY.
+        if "ERRORCODE" not in error or "ERRORMESSAGE" not in error:
+            return None
+        if error["ERRORCODE"] ==  4:
+          return InvalidCredsException(error["ERRORMESSAGE"])
+        return LinodeException(error["ERRORCODE"], error["ERRORMESSAGE"])
+        
+
+class LinodeConnection(ConnectionKey):
+    # Wraps a Linode HTTPS connection, and passes along the connection key.
+    host = LINODE_API
+    responseCls = LinodeResponse
+    def add_default_params(self, params):
+        params["api_key"] = self.key
+        # Be explicit about this in case the default changes.
+        params["api_responseFormat"] = "json"
+        return params
+
+
+class LinodeNodeDriver(NodeDriver):
+    # The meat of Linode operations; the Node Driver.
+    type = Provider.LINODE
+    name = "Linode"
+    connectionCls = LinodeConnection
+    
+    def __init__(self, key):
+        self.datacenter = None
+        NodeDriver.__init__(self, key)
+
+    # Converts Linode's state from DB to a NodeState constant.
+    # Some of these are lightly questionable.
+    LINODE_STATES = {
+        -2: NodeState.UNKNOWN,              # Boot Failed
+        -1: NodeState.PENDING,              # Being Created
+         0: NodeState.PENDING,              # Brand New
+         1: NodeState.RUNNING,              # Running
+         2: NodeState.REBOOTING,            # Powered Off (TODO: Extra state?)
+         3: NodeState.REBOOTING,            # Shutting Down (?)
+         4: NodeState.UNKNOWN               # Reserved
+    }
+
+    def list_nodes(self):
+        # List
+        # Provide a list of all nodes that this API key has access to.
+        params = { "api_action": "linode.list" }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        return [self._to_node(n) for n in data]
+    
+    def reboot_node(self, node):
+        # Reboot
+        # Execute a shutdown and boot job for the given Node.
+        params = { "api_action": "linode.reboot", "LinodeID": node.id }
+        self.connection.request(LINODE_ROOT, params=params)
+        return True
+    
+    def destroy_node(self, node):
+        # Destroy
+        # Terminates a Node.  With prejudice.
+        params = { "api_action": "linode.delete", "LinodeID": node.id,
+            "skipChecks": True }
+        self.connection.request(LINODE_ROOT, params=params)
+        return True
+
+    def create_node(self, **kwargs):
+        """Create a new linode instance
+
+        See L{NodeDriver.create_node} for more keyword args.
+
+        @keyword    swap: Size of the swap partition in MB (128).
+        @type       swap: C{number}
+
+        @keyword    rsize: Size of the root partition (plan size - swap).
+        @type       rsize: C{number}
+
+        @keyword    kernel: A kernel ID from avail.kernels (Latest 2.6).
+        @type       kernel: C{number}
+
+        @keyword    payment: One of 1, 12, or 24; subscription length (1)
+        @type       payment: C{number}
+
+        @keyword    comment: Comments to store with the config
+        @type       comment: C{str}
+        """
+        #    Labels to override what's generated (default on right):
+        #       lconfig      [%name] Instance
+        #       lrecovery    [%name] Finnix Recovery Configuration
+        #       lroot        [%name] %distro
+        #       lswap        [%name] Swap Space
+        #
+        # Datacenter logic:
+        #
+        #   As Linode requires choosing a datacenter, a little logic is done.
+        #
+        #   1. If the API key in use has all its Linodes in one DC, that DC will
+        #      be chosen (and can be overridden with linode_set_datacenter).
+        #
+        #   2. Otherwise (for both the "No Linodes" and "different DC" cases), a
+        #      datacenter must explicitly be chosen using linode_set_datacenter.
+        #
+        # Please note that for safety, only 5 Linodes can be created per hour.
+
+        name = kwargs["name"]
+        chosen = kwargs["location"].id
+        image = kwargs["image"]
+        size = kwargs["size"]
+        auth = kwargs["auth"]
+
+        # Step 0: Parameter validation before we purchase
+        # We're especially careful here so we don't fail after purchase, rather
+        # than getting halfway through the process and having the API fail.
+
+        # Plan ID
+        plans = self.list_sizes()
+        if size.id not in [p.id for p in plans]:
+            raise LinodeException(0xFB, "Invalid plan ID -- avail.plans")
+
+        # Payment schedule
+        payment = "1" if "payment" not in kwargs else str(kwargs["payment"])
+        if payment not in ["1", "12", "24"]:
+            raise LinodeException(0xFB, "Invalid subscription (1, 12, 24)")
+
+        ssh = None
+        root = None
+        # SSH key and/or root password
+        if isinstance(auth, NodeAuthSSHKey):
+            ssh = auth.pubkey
+        elif isinstance(auth, NodeAuthPassword):
+            root = auth.password
+
+        if not ssh and not root:
+            raise LinodeException(0xFB, "Need SSH key or root password")
+        if len(root) < 6:
+            raise LinodeException(0xFB, "Root password is too short")
+
+        # Swap size
+        try: swap = 128 if "swap" not in kwargs else int(kwargs["swap"])
+        except: raise LinodeException(0xFB, "Need an integer swap size")
+
+        # Root partition size
+        imagesize = (size.disk - swap) if "rsize" not in kwargs else \
+            int(kwargs["rsize"])
+        if (imagesize + swap) > size.disk:
+            raise LinodeException(0xFB, "Total disk images are too big")
+
+        # Distribution ID
+        distros = self.list_images()
+        if image.id not in [d.id for d in distros]:
+            raise LinodeException(0xFB,
+                                  "Invalid distro -- avail.distributions")
+
+        # Kernel
+        kernel = 60 if "kernel" not in kwargs else kwargs["kernel"]
+        params = { "api_action": "avail.kernels" }
+        kernels = self.connection.request(LINODE_ROOT, params=params).object
+        if kernel not in [z["KERNELID"] for z in kernels]:
+            raise LinodeException(0xFB, "Invalid kernel -- avail.kernels")
+
+        # Comments
+        comments = "Created by libcloud <http://www.libcloud.org>" if \
+            "comment" not in kwargs else kwargs["comment"]
+
+        # Labels
+        label = {
+            "lconfig": "[%s] Configuration Profile" % name,
+            "lrecovery": "[%s] Finnix Recovery Configuration" % name,
+            "lroot": "[%s] %s Disk Image" % (name, image.name),
+            "lswap": "[%s] Swap Space" % name
+        }
+        for what in ["lconfig", "lrecovery", "lroot", "lswap"]:
+            if what in kwargs:
+                label[what] = kwargs[what]
+
+        # Step 1: linode.create
+        params = {
+            "api_action":   "linode.create",
+            "DatacenterID": chosen,
+            "PlanID":       size.id,
+            "PaymentTerm":  payment
+        }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        linode = { "id": data["LinodeID"] }
+
+        # Step 2: linode.disk.createfromdistribution
+        if not root:
+            root = os.urandom(16).encode('hex')
+        params = {
+            "api_action":       "linode.disk.createfromdistribution",
+            "LinodeID":         linode["id"],
+            "DistributionID":   image.id,
+            "Label":            label["lroot"],
+            "Size":             imagesize,
+            "rootPass":         root,
+        }
+        if ssh: params["rootSSHKey"] = ssh
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        linode["rootimage"] = data["DiskID"]
+
+        # Step 3: linode.disk.create for swap
+        params = {
+            "api_action":       "linode.disk.create",
+            "LinodeID":         linode["id"],
+            "Label":            label["lswap"],
+            "Type":             "swap",
+            "Size":             swap
+        }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        linode["swapimage"] = data["DiskID"]
+
+        # Step 4: linode.config.create for main profile
+        disks = "%s,%s,,,,,,," % (linode["rootimage"], linode["swapimage"])
+        params = {
+            "api_action":       "linode.config.create",
+            "LinodeID":         linode["id"],
+            "KernelID":         kernel,
+            "Label":            label["lconfig"],
+            "Comments":         comments,
+            "DiskList":         disks
+        }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        linode["config"] = data["ConfigID"]
+
+        # TODO: Recovery image (Finnix)
+
+        # Step 5: linode.boot
+        params = {
+            "api_action":       "linode.boot",
+            "LinodeID":         linode["id"],
+            "ConfigID":         linode["config"]
+        }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+
+        # Make a node out of it and hand it back
+        params = { "api_action": "linode.list", "LinodeID": linode["id"] }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        return self._to_node(data[0])
+
+    def list_sizes(self, location=None):
+        # List Sizes
+        # Retrieve all available Linode plans.
+        # FIXME: Prices get mangled due to 'float'.
+        params = { "api_action": "avail.linodeplans" }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        sizes = []
+        for obj in data:
+            n = NodeSize(id=obj["PLANID"], name=obj["LABEL"], ram=obj["RAM"],
+                    disk=(obj["DISK"] * 1024), bandwidth=obj["XFER"],
+                    price=obj["PRICE"], driver=self.connection.driver)
+            sizes.append(n)
+        return sizes
+    
+    def list_images(self, location=None):
+        # List Images
+        # Retrieve all available Linux distributions.
+        params = { "api_action": "avail.distributions" }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        distros = []
+        for obj in data:
+            i = NodeImage(id=obj["DISTRIBUTIONID"], name=obj["LABEL"],
+                driver=self.connection.driver)
+            distros.append(i)
+        return distros
+
+    def list_locations(self):
+        params = { "api_action": "avail.datacenters" }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        nl = []
+        for dc in data:
+            country = None
+            #TODO: this is a hack!
+            if dc["LOCATION"][-3:] == "USA":
+                country = "US"
+            elif dc["LOCATION"][-2:] == "UK":
+                country = "GB"
+            else:
+                raise LinodeException(
+                    0xFD,
+                    "Unable to convert data center location to country: '%s'"
+                    % dc["LOCATION"]
+                )
+            nl.append(NodeLocation(dc["DATACENTERID"],
+                                   dc["LOCATION"],
+                                   country,
+                                   self))
+        return nl
+
+    def linode_set_datacenter(self, did):
+        # Set the datacenter for create requests.
+        #
+        # Create will try to guess, based on where all of the API key's
+        # Linodes are located; if they are all in one location, Create will
+        # make a new node there.  If there are NO Linodes on the account or
+        # Linodes are in multiple locations, it is imperative to set this or
+        # creates will fail.
+        params = { "api_action": "avail.datacenters" }
+        data = self.connection.request(LINODE_ROOT, params=params).object
+        for dc in data:
+            if did == dc["DATACENTERID"]:
+                self.datacenter = did
+                return
+
+        dcs = ", ".join([d["DATACENTERID"] for d in data])
+        self.datacenter = None
+        raise LinodeException(0xFD, "Invalid datacenter (use one of %s)" % dcs)
+
+    def _to_node(self, obj):
+        # Convert a returned Linode instance into a Node instance.
+        lid = obj["LINODEID"]
+        
+        # Get the IP addresses for a Linode
+        params = { "api_action": "linode.ip.list", "LinodeID": lid }        
+        req = self.connection.request(LINODE_ROOT, params=params)
+        if not req.success() or len(req.object) == 0:
+            return None
+        
+        public_ip = []
+        private_ip = []
+        for ip in req.object:
+            if ip["ISPUBLIC"]:
+              public_ip.append(ip["IPADDRESS"])
+            else:
+              private_ip.append(ip["IPADDRESS"])
+
+        n = Node(id=lid, name=obj["LABEL"],
+            state=self.LINODE_STATES[obj["STATUS"]], public_ip=public_ip,
+            private_ip=private_ip, driver=self.connection.driver)
+        n.extra = copy(obj)
+        return n
+
+    features = {"create_node": ["ssh_key", "password"]}
diff --git a/libcloud/drivers/rackspace.py b/libcloud/drivers/rackspace.py
new file mode 100644
index 0000000..5f81e95
--- /dev/null
+++ b/libcloud/drivers/rackspace.py
@@ -0,0 +1,308 @@
+# 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.
+# libcloud.org 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.
+"""
+Rackspace driver
+"""
+from libcloud.types import NodeState, InvalidCredsException, Provider
+from libcloud.base import ConnectionUserAndKey, Response, NodeDriver, Node
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+import os
+
+import base64
+import urlparse
+
+from xml.etree import ElementTree as ET
+from xml.parsers.expat import ExpatError
+
+NAMESPACE = 'http://docs.rackspacecloud.com/servers/api/v1.0'
+
+class RackspaceResponse(Response):
+
+    def success(self):
+        i = int(self.status)
+        return i >= 200 and i <= 299
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        return ET.XML(self.body)
+
+    def parse_error(self):
+        # TODO: fixup, Rackspace only uses response codes really!
+        try:
+            object = ET.XML(self.body)
+            text = "; ".join([ err.text or ''
+                               for err in
+                               object.getiterator()
+                               if err.text])
+        except ExpatError:
+            text = self.body
+        return '%s %s %s' % (self.status, self.error, text)
+
+
+class RackspaceConnection(ConnectionUserAndKey):
+    api_version = 'v1.0'
+    auth_host = 'auth.api.rackspacecloud.com'
+    responseCls = RackspaceResponse
+
+    def __init__(self, user_id, key, secure=True):
+        self.__host = None
+        self.path = None
+        self.token = None
+        super(RackspaceConnection, self).__init__(user_id, key, secure)
+
+    def add_default_headers(self, headers):
+        headers['X-Auth-Token'] = self.token;
+        headers['Accept'] = 'application/xml'
+        return headers
+
+    @property
+    def host(self):
+        """
+        Rackspace uses a separate host for API calls which is only provided
+        after an initial authentication request. If we haven't made that
+        request yet, do it here. Otherwise, just return the management host.
+
+        TODO: Fixup for when our token expires (!!!)
+        """
+        if not self.__host:
+            # Initial connection used for authentication
+            conn = self.conn_classes[self.secure](self.auth_host, self.port[self.secure])
+            conn.request(
+                method='GET',
+                url='/%s' % self.api_version,
+                headers={
+                    'X-Auth-User': self.user_id,
+                    'X-Auth-Key': self.key
+                }
+            )
+            resp = conn.getresponse()
+            headers = dict(resp.getheaders())
+            try:
+                self.token = headers['x-auth-token']
+                endpoint = headers['x-server-management-url']
+            except KeyError:
+                raise InvalidCredsException()
+
+            scheme, server, self.path, param, query, fragment = (
+                urlparse.urlparse(endpoint)
+            )
+            if scheme is "https" and self.secure is not 1:
+                # TODO: Custom exception (?)
+                raise InvalidCredsException()
+
+            # Set host to where we want to make further requests to;
+            # close auth conn
+            self.__host = server
+            conn.close()
+
+        return self.__host
+
+    def request(self, action, params={}, data='', headers={}, method='GET'):
+        # Due to first-run authentication request, we may not have a path
+        if self.path:
+            action = self.path + action
+        if method == "POST":
+            headers = {'Content-Type': 'application/xml; charset=UTF-8'}
+        if method == "GET":
+          params['cache-busting'] = os.urandom(8).encode('hex')
+        return super(RackspaceConnection, self).request(
+            action=action,
+            params=params, data=data,
+            method=method, headers=headers
+        )
+
+
+class RackspaceNodeDriver(NodeDriver):
+    """
+    Rackspace node driver.
+    
+    Extra node attributes:
+        - password: root password, available after create.
+        - hostId: represents the host your cloud server runs on
+        - imageId: id of image
+        - flavorId: id of flavor
+    """
+    connectionCls = RackspaceConnection
+    type = Provider.RACKSPACE
+    name = 'Rackspace'
+
+    NODE_STATE_MAP = { 'BUILD': NodeState.PENDING,
+                       'ACTIVE': NodeState.RUNNING,
+                       'SUSPENDED': NodeState.TERMINATED,
+                       'QUEUE_RESIZE': NodeState.PENDING,
+                       'PREP_RESIZE': NodeState.PENDING,
+                       'RESCUE': NodeState.PENDING,
+                       'REBUILD': NodeState.PENDING,
+                       'REBOOT': NodeState.REBOOTING,
+                       'HARD_REBOOT': NodeState.REBOOTING}
+
+    def list_nodes(self):
+        return self.to_nodes(self.connection.request('/servers/detail').object)
+
+    def list_sizes(self, location=None):
+        return self.to_sizes(self.connection.request('/flavors/detail').object)
+
+    def list_images(self, location=None):
+        return self.to_images(self.connection.request('/images/detail').object)
+
+    def list_locations(self):
+        return [NodeLocation(0, "Rackspace DFW1", 'US', self)]
+
+    def create_node(self, **kwargs):
+        """Create a new rackspace node
+
+        See L{NodeDriver.create_node} for more keyword args.
+        @keyword    metadata: Key/Value metadata to associate with a node
+        @type       metadata: C{dict}
+
+        @keyword    file:   File Path => File contents to create on the node
+        @type       file:   C{dict}
+        """
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+        server_elm = ET.Element(
+            'server',
+            {'xmlns': NAMESPACE,
+             'name': name,
+             'imageId': str(image.id),
+             'flavorId': str(size.id)}
+        )
+
+        metadata_elm = self._metadata_to_xml(kwargs.get("metadata", {}))
+        if metadata_elm:
+            server_elm.append(metadata_elm)
+
+        files_elm = self._files_to_xml(kwargs.get("files", {}))
+        if files_elm:
+            server_elm.append(files_elm)
+
+        resp = self.connection.request("/servers", 
+                                       method='POST', 
+                                       data=ET.tostring(server_elm))
+        return self._to_node(resp.object)
+      
+    def _metadata_to_xml(self, metadata):
+        if len(metadata) == 0:
+            return None
+
+        metadata_elm = ET.Element('metadata')
+        for k, v in metadata.items():
+            meta_elm = ET.SubElement(metadata_elm, 'meta', {'key': str(k) })
+            meta_elm.text = str(v)
+
+        return metadata_elm
+  
+    def _files_to_xml(self, files):
+        if len(files) == 0:
+            return None
+
+        personality_elm = ET.Element('personality')
+        for k, v in files.items():
+            file_elm = ET.SubElement(personality_elm,
+                                     'file',
+                                     {'path': str(k)})
+            file_elm.text = base64.b64encode(v)
+
+        return personality_elm
+
+    def reboot_node(self, node):
+        # TODO: Hard Reboots should be supported too!
+        resp = self._node_action(node, ['reboot', ('type', 'SOFT')])
+        return resp.status == 202
+
+    def destroy_node(self, node):
+        uri = '/servers/%s' % (node.id)
+        resp = self.connection.request(uri, method='DELETE')
+        return resp.status == 202
+
+    def _node_action(self, node, body):
+        if isinstance(body, list):
+            attr = ' '.join(['%s="%s"' % (item[0], item[1])
+                             for item in body[1:]])
+            body = '<%s xmlns="%s" %s/>' % (body[0], NAMESPACE, attr)
+        uri = '/servers/%s/action' % (node.id)
+        resp = self.connection.request(uri, method='POST', data=body)
+        return resp
+
+    def to_nodes(self, object):
+        node_elements = self._findall(object, 'server')
+        return [ self._to_node(el) for el in node_elements ]
+
+    def _fixxpath(self, xpath):
+        # ElementTree wants namespaces in its xpaths, so here we add them.
+        return "/".join(["{%s}%s" % (NAMESPACE, e) for e in xpath.split("/")])
+
+    def _findall(self, element, xpath):
+        return element.findall(self._fixxpath(xpath))
+
+    def _to_node(self, el):
+        def get_ips(el):
+            return [ip.get('addr') for ip in el]
+          
+        def get_meta_dict(el):
+            d = {}
+            for meta in el:
+                d[meta.get('key')] =  meta.text
+            return d
+        
+        public_ip = get_ips(self._findall(el, 
+                                          'addresses/public/ip'))
+        private_ip = get_ips(self._findall(el, 
+                                          'addresses/private/ip'))
+        metadata = get_meta_dict(self._findall(el, 'metadata/meta'))
+        
+        n = Node(id=el.get('id'),
+                 name=el.get('name'),
+                 state=el.get('status'),
+                 public_ip=public_ip,
+                 private_ip=private_ip,
+                 driver=self.connection.driver,
+                 extra={
+                    'password': el.get('adminPass'),
+                    'hostId': el.get('hostId'),
+                    'imageId': el.get('imageId'),
+                    'flavorId': el.get('flavorId'),
+                    'metadata': metadata,
+                 })
+        return n
+
+    def to_sizes(self, object):
+        elements = self._findall(object, 'flavor')
+        return [ self._to_size(el) for el in elements ]
+
+    def _to_size(self, el):
+        s = NodeSize(id=el.get('id'),
+                     name=el.get('name'),
+                     ram=int(el.get('ram')),
+                     disk=int(el.get('disk')),
+                     bandwidth=None, # XXX: needs hardcode
+                     price=None, # XXX: needs hardcode,
+                     driver=self.connection.driver)
+        return s
+
+    def to_images(self, object):
+        elements = self._findall(object, "image")
+        return [ self._to_image(el)
+                 for el in elements
+                 if el.get('status') == 'ACTIVE' ]
+
+    def _to_image(self, el):
+        i = NodeImage(id=el.get('id'),
+                     name=el.get('name'),
+                     driver=self.connection.driver,
+                     extra={'serverId': el.get('serverId')})
+        return i
diff --git a/libcloud/drivers/rimuhosting.py b/libcloud/drivers/rimuhosting.py
new file mode 100644
index 0000000..0aebd7e
--- /dev/null
+++ b/libcloud/drivers/rimuhosting.py
@@ -0,0 +1,294 @@
+# 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.
+# libcloud.org 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.
+# Copyright 2009 RedRata Ltd
+"""
+RimuHosting Driver
+"""
+from libcloud.types import Provider, NodeState, InvalidCredsException
+from libcloud.base import ConnectionKey, Response, NodeAuthPassword
+from libcloud.base import NodeDriver, NodeSize, Node, NodeLocation
+from libcloud.base import NodeImage
+
+# JSON is included in the standard library starting with Python 2.6.  For 2.5
+# and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson
+try: import json
+except: import simplejson as json
+
+# Defaults
+API_CONTEXT = '/r'
+API_HOST = 'api.rimuhosting.com'
+API_PORT = (80,443)
+API_SECURE = True
+
+class RimuHostingException(Exception):
+    def __str__(self):
+        return self.args[0]
+
+    def __repr__(self):
+        return "<RimuHostingException '%s'>" % (self.args[0])
+
+class RimuHostingResponse(Response):
+    def __init__(self, response):
+        self.body = response.read()
+        self.status = response.status
+        self.headers = dict(response.getheaders())
+        self.error = response.reason
+
+        if self.success():
+          self.object = self.parse_body()
+
+    def success(self):
+        if self.status == 403:
+          raise InvalidCredsException()
+        return True
+    def parse_body(self):
+        try:
+            js = json.loads(self.body)
+            if js[js.keys()[0]]['response_type'] == "ERROR":
+                raise RimuHostingException(
+                    js[js.keys()[0]]['human_readable_message']
+                )
+            return js[js.keys()[0]]
+        except ValueError:
+            raise RimuHostingException('Could not parse body: %s'
+                                       % (self.body))
+        except KeyError:
+            raise RimuHostingException('Could not parse body: %s'
+                                       % (self.body))
+    
+class RimuHostingConnection(ConnectionKey):
+    
+    api_context = API_CONTEXT
+    host = API_HOST
+    port = API_PORT
+    responseCls = RimuHostingResponse
+    
+    def __init__(self, key, secure=True):
+        # override __init__ so that we can set secure of False for testing
+        ConnectionKey.__init__(self,key,secure)
+
+    def add_default_headers(self, headers):
+        # We want JSON back from the server. Could be application/xml
+        # (but JSON is better).
+        headers['Accept'] = 'application/json'
+        # Must encode all data as json, or override this header.
+        headers['Content-Type'] = 'application/json'
+      
+        headers['Authorization'] = 'rimuhosting apikey=%s' % (self.key)
+        return headers;
+
+    def request(self, action, params={}, data='', headers={}, method='GET'):
+        # Override this method to prepend the api_context
+        return ConnectionKey.request(self, self.api_context + action,
+                                     params, data, headers, method)
+
+class RimuHostingNodeDriver(NodeDriver):
+    type = Provider.RIMUHOSTING
+    name = 'RimuHosting'
+    connectionCls = RimuHostingConnection
+    
+    def __init__(self, key, host=API_HOST, port=API_PORT,
+                 api_context=API_CONTEXT, secure=API_SECURE):
+        # Pass in some extra vars so that
+        self.key = key
+        self.secure = secure
+        self.connection = self.connectionCls(key ,secure)
+        self.connection.host = host
+        self.connection.api_context = api_context
+        self.connection.port = port
+        self.connection.driver = self
+        self.connection.connect()
+
+    def _order_uri(self, node,resource):
+        # Returns the order uri with its resourse appended.
+        return "/orders/%s/%s" % (node.id,resource)
+   
+    # TODO: Get the node state.
+    def _to_node(self, order):
+        n = Node(id=order['slug'],
+                name=order['domain_name'],
+                state=NodeState.RUNNING,
+                public_ip=(
+                    [order['allocated_ips']['primary_ip']]
+                    + order['allocated_ips']['secondary_ips']
+                ),
+                private_ip=[],
+                driver=self.connection.driver,
+                extra={'order_oid': order['order_oid']})
+        return n
+
+    def _to_size(self,plan):
+        return NodeSize(
+            id=plan['pricing_plan_code'],
+            name=plan['pricing_plan_description'],
+            ram=plan['minimum_memory_mb'],
+            disk=plan['minimum_disk_gb'],
+            bandwidth=plan['minimum_data_transfer_allowance_gb'],
+            price=plan['monthly_recurring_amt']['amt_usd'],
+            driver=self.connection.driver
+        )
+                
+    def _to_image(self,image):
+        return NodeImage(id=image['distro_code'],
+            name=image['distro_description'],
+            driver=self.connection.driver)
+        
+    def list_sizes(self, location=None):
+        # Returns a list of sizes (aka plans)
+        # Get plans. Note this is really just for libcloud.
+        # We are happy with any size.
+        if location == None:
+            location = '';
+        else:
+            location = ";dc_location=%s" % (location.id)
+
+        res = self.connection.request('/pricing-plans;server-type=VPS%s' % (location)).object
+        return map(lambda x : self._to_size(x), res['pricing_plan_infos'])
+
+    def list_nodes(self):
+        # Returns a list of Nodes
+        # Will only include active ones.
+        res = self.connection.request('/orders;include_inactive=N').object
+        return map(lambda x : self._to_node(x), res['about_orders'])
+    
+    def list_images(self, location=None):
+        # Get all base images.
+        # TODO: add other image sources. (Such as a backup of a VPS)
+        # All Images are available for use at all locations
+        res = self.connection.request('/distributions').object
+        return map(lambda x : self._to_image(x), res['distro_infos'])
+
+    def reboot_node(self, node):
+        # Reboot
+        # PUT the state of RESTARTING to restart a VPS.
+        # All data is encoded as JSON
+        data = {'reboot_request':{'running_state':'RESTARTING'}}
+        uri = self._order_uri(node,'vps/running-state')
+        self.connection.request(uri,data=json.dumps(data),method='PUT')
+        # XXX check that the response was actually successful
+        return True
+    
+    def destroy_node(self, node):
+        # Shutdown a VPS.
+        uri = self._order_uri(node,'vps')
+        self.connection.request(uri,method='DELETE')
+        # XXX check that the response was actually successful
+        return True
+
+    def create_node(self, **kwargs):
+        # Creates a RimuHosting instance
+        #
+        #   name    Must be a FQDN. e.g example.com.
+        #   image   NodeImage from list_images
+        #   size    NodeSize from list_sizes
+        #
+        # Keyword arguements supported:
+        #
+        #   billing_oid        If not set, a billing method is automatically
+        #                      picked.
+        #
+        #   host_server_oid         The host server to set the VPS up on.
+        #   vps_order_oid_to_clone  Clone another VPS to use as the image
+        #                           for the new VPS.
+        #  
+        #   num_ips = 1        Number of IPs to allocate. Defaults to 1.
+        #   extra_ip_reason    Reason for needing the extra IPS.
+        #   
+        #   memory_mb          Memory to allocate to the VPS.
+        #   disk_space_mb      Diskspace to allocate to the VPS.
+        #                      Defaults to 4096 (4GB).
+        #   disk_space_2_mb    Secondary disk size allocation.
+        #                      Disabled by default.
+        #   
+        #   pricing_plan_code  Plan from list_sizes
+        #   
+        #   control_panel      Control panel to install on the VPS.
+        #
+        #
+        # Note we don't do much error checking in this because we
+        # expect the API to error out if there is a problem.
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+
+        data = {
+            'instantiation_options':{
+                'domain_name': name, 'distro': image.id
+            },
+            'pricing_plan_code': size.id,
+        }
+        
+        if kwargs.has_key('control_panel'):
+            data['instantiation_options']['control_panel'] = kwargs['control_panel']
+
+        if kwargs.has_key('auth'):
+            auth = kwargs['auth']
+            if not isinstance(auth, NodeAuthPassword):
+                raise ValueError('auth must be of NodeAuthPassword type')
+            data['instantiation_options']['password'] = auth.password
+        
+        if kwargs.has_key('billing_oid'):
+            #TODO check for valid oid.
+            data['billing_oid'] = kwargs['billing_oid']
+        
+        if kwargs.has_key('host_server_oid'):
+            data['host_server_oid'] = kwargs['host_server_oid']
+            
+        if kwargs.has_key('vps_order_oid_to_clone'):
+            data['vps_order_oid_to_clone'] = kwargs['vps_order_oid_to_clone']
+        
+        if kwargs.has_key('num_ips') and int(kwargs['num_ips']) > 1:
+            if not kwargs.has_key('extra_ip_reason'):
+                raise RimuHostingException('Need an reason for having an extra IP')
+            else:
+                if not data.has_key('ip_request'):
+                    data['ip_request'] = {}
+                data['ip_request']['num_ips'] = int(kwargs['num_ips'])
+                data['ip_request']['extra_ip_reason'] = kwargs['extra_ip_reason']
+        
+        if kwargs.has_key('memory_mb'):
+            if not data.has_key('vps_parameters'):
+                data['vps_parameters'] = {}
+            data['vps_parameters']['memory_mb'] = kwargs['memory_mb']
+        
+        if kwargs.has_key('disk_space_mb'):
+            if not data.has_key('vps_parameters'):
+                data['vps_parameters'] = {}
+            data['vps_parameters']['disk_space_mb'] = kwargs['disk_space_mb']
+        
+        if kwargs.has_key('disk_space_2_mb'):
+            if not data.has_key('vps_parameters'):
+                data['vps_parameters'] = {}
+            data['vps_parameters']['disk_space_2_mb'] = kwargs['disk_space_2_mb']
+        
+        res = self.connection.request(
+            '/orders/new-vps',
+            method='POST',
+            data=json.dumps({"new-vps":data})
+        ).object
+        node = self._to_node(res['about_order'])
+        node.extra['password'] = res['new_order_request']['instantiation_options']['password']
+        return node
+    
+    def list_locations(self):
+        return [
+            NodeLocation('DCAUCKLAND', "RimuHosting Auckland", 'NZ', self),
+            NodeLocation('DCDALLAS', "RimuHosting Dallas", 'US', self),
+            NodeLocation('DCLONDON', "RimuHosting London", 'GB', self),
+            NodeLocation('DCSYDNEY', "RimuHosting Sydney", 'AU', self),
+        ]
+
+    features = {"create_node": ["password"]}
+        
diff --git a/libcloud/drivers/slicehost.py b/libcloud/drivers/slicehost.py
new file mode 100644
index 0000000..def2335
--- /dev/null
+++ b/libcloud/drivers/slicehost.py
@@ -0,0 +1,226 @@
+# 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.
+# libcloud.org 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.
+"""
+Slicehost Driver
+"""
+from libcloud.types import NodeState, Provider
+from libcloud.base import ConnectionKey, Response, NodeDriver, Node
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+import base64
+import struct
+import socket
+from xml.etree import ElementTree as ET
+from xml.parsers.expat import ExpatError
+
+class SlicehostResponse(Response):
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        return ET.XML(self.body)
+
+    def parse_error(self):
+        try:
+            object = ET.XML(self.body)
+            return "; ".join([ err.text
+                               for err in
+                               object.findall('error') ])
+        except ExpatError:
+            return self.body
+    
+
+class SlicehostConnection(ConnectionKey):
+
+    host = 'api.slicehost.com'
+    responseCls = SlicehostResponse
+
+    def add_default_headers(self, headers):
+        headers['Authorization'] = ('Basic %s'
+                              % (base64.b64encode('%s:' % self.key)))
+        return headers
+    
+
+class SlicehostNodeDriver(NodeDriver):
+
+    connectionCls = SlicehostConnection
+
+    type = Provider.SLICEHOST
+    name = 'Slicehost'
+
+    NODE_STATE_MAP = { 'active': NodeState.RUNNING,
+                       'build': NodeState.PENDING,
+                       'reboot': NodeState.REBOOTING,
+                       'hard_reboot': NodeState.REBOOTING,
+                       'terminated': NodeState.TERMINATED }
+
+    def list_nodes(self):
+        return self._to_nodes(self.connection.request('/slices.xml').object)
+
+    def list_sizes(self, location=None):
+        return self._to_sizes(self.connection.request('/flavors.xml').object)
+
+    def list_images(self, location=None):
+        return self._to_images(self.connection.request('/images.xml').object)
+
+    def list_locations(self):
+        return [
+            NodeLocation(0, 'Slicehost St. Louis (STL-A)', 'US', self),
+            NodeLocation(0, 'Slicehost St. Louis (STL-B)', 'US', self),
+            NodeLocation(0, 'Slicehost Dallas-Fort Worth (DFW-1)', 'US', self)
+        ]
+
+    def create_node(self, **kwargs):
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+        uri = '/slices.xml'
+
+        # create a slice obj
+        root = ET.Element('slice')
+        el_name = ET.SubElement(root, 'name')
+        el_name.text = name
+        flavor_id = ET.SubElement(root, 'flavor-id')
+        flavor_id.text = str(size.id)
+        image_id = ET.SubElement(root, 'image-id')
+        image_id.text = str(image.id)
+        xml = ET.tostring(root)
+
+        node = self._to_nodes(
+            self.connection.request(
+                uri,
+                method='POST',
+                data=xml,
+                headers={'Content-Type': 'application/xml'}
+            ).object
+        )[0]
+        return node
+
+    def reboot_node(self, node):
+        """Reboot the node by passing in the node object"""
+
+        # 'hard' could bubble up as kwarg depending on how reboot_node 
+        # turns out. Defaulting to soft reboot.
+        #hard = False
+        #reboot = self.api.hard_reboot if hard else self.api.reboot
+        #expected_status = 'hard_reboot' if hard else 'reboot'
+
+        uri = '/slices/%s/reboot.xml' % (node.id)
+        node = self._to_nodes(
+            self.connection.request(uri, method='PUT').object
+        )[0]
+        return node.state == NodeState.REBOOTING
+
+    def destroy_node(self, node):
+        """Destroys the node
+
+        Requires 'Allow Slices to be deleted or rebuilt from the API' to be
+        ticked at https://manage.slicehost.com/api, otherwise returns::
+            <errors>
+              <error>You must enable slice deletes in the SliceManager</error>
+              <error>Permission denied</error>
+            </errors>
+        """
+        uri = '/slices/%s/destroy.xml' % (node.id)
+        ret = self.connection.request(uri, method='PUT')
+        return True
+
+    def _to_nodes(self, object):
+        if object.tag == 'slice':
+            return [ self._to_node(object) ]
+        node_elements = object.findall('slice')
+        return [ self._to_node(el) for el in node_elements ]
+
+    def _to_node(self, element):
+
+        attrs = [ 'name', 'image-id', 'progress', 'id', 'bw-out', 'bw-in', 
+                  'flavor-id', 'status', 'ip-address' ]
+
+        node_attrs = {}
+        for attr in attrs:
+            node_attrs[attr] = element.findtext(attr)
+
+        # slicehost does not determine between public and private, so we 
+        # have to figure it out
+        public_ip = element.findtext('ip-address')
+        private_ip = None
+        for addr in element.findall('addresses/address'):
+            ip = addr.text
+            try:
+                socket.inet_aton(ip)
+            except socket.error:
+                # not a valid ip
+                continue
+            if self._is_private_subnet(ip):
+                private_ip = ip
+            else:
+                public_ip = ip
+                
+        try:
+            state = self.NODE_STATE_MAP[element.findtext('status')]
+        except:
+            state = NodeState.UNKNOWN
+
+        n = Node(id=element.findtext('id'),
+                 name=element.findtext('name'),
+                 state=state,
+                 public_ip=[public_ip],
+                 private_ip=[private_ip],
+                 driver=self.connection.driver)
+        return n
+
+    def _to_sizes(self, object):
+        if object.tag == 'flavor':
+            return [ self._to_size(object) ]
+        elements = object.findall('flavor')
+        return [ self._to_size(el) for el in elements ]
+
+    def _to_size(self, element):
+        s = NodeSize(id=int(element.findtext('id')),
+                     name=str(element.findtext('name')),
+                     ram=int(element.findtext('ram')),
+                     disk=None, # XXX: needs hardcode
+                     bandwidth=None, # XXX: needs hardcode
+                     price=float(element.findtext('price'))/(100*24*30),
+                     driver=self.connection.driver)
+        return s
+
+    def _to_images(self, object):
+        if object.tag == 'image':
+            return [ self._to_image(object) ]
+        elements = object.findall('image')
+        return [ self._to_image(el) for el in elements ]
+
+    def _to_image(self, element):
+        i = NodeImage(id=int(element.findtext('id')),
+                     name=str(element.findtext('name')),
+                     driver=self.connection.driver)
+        return i
+
+
+    def _is_private_subnet(self, ip):
+        priv_subnets = [ {'subnet': '10.0.0.0', 'mask': '255.0.0.0'},
+                         {'subnet': '172.16.0.0', 'mask': '172.16.0.0'},
+                         {'subnet': '192.168.0.0', 'mask': '192.168.0.0'} ]
+
+        ip = struct.unpack('I',socket.inet_aton(ip))[0]
+
+        for network in priv_subnets:
+            subnet = struct.unpack('I',socket.inet_aton(network['subnet']))[0]
+            mask = struct.unpack('I',socket.inet_aton(network['mask']))[0]
+
+            if (ip & mask) == (subnet & mask):
+                return True
+            
+        return False
diff --git a/libcloud/drivers/vcloud.py b/libcloud/drivers/vcloud.py
new file mode 100644
index 0000000..1079b0f
--- /dev/null
+++ b/libcloud/drivers/vcloud.py
@@ -0,0 +1,587 @@
+# 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.
+# libcloud.org 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.
+"""
+vmware VCloud driver.
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver
+from libcloud.base import NodeSize, NodeImage, NodeAuthPassword, NodeLocation
+
+import base64
+import httplib
+import time
+from urlparse import urlparse
+from xml.etree import ElementTree as ET
+from xml.parsers.expat import ExpatError
+
+"""
+From vcloud api "The VirtualQuantity element defines the number of MB
+of memory. This should be either 512 or a multiple of 1024 (1 GB)."
+"""
+VIRTUAL_MEMORY_VALS = [512] + [1024 * i for i in range(1,9)]
+
+DEFAULT_TASK_COMPLETION_TIMEOUT = 600
+
+def get_url_path(url):
+    return urlparse(url.strip()).path
+
+def fixxpath(root, xpath):
+    """ElementTree wants namespaces in its xpaths, so here we add them."""
+    namespace, root_tag = root.tag[1:].split("}", 1)
+    fixed_xpath = "/".join(["{%s}%s" % (namespace, e)
+                            for e in xpath.split("/")])
+    return fixed_xpath
+
+class InstantiateVAppXML(object):
+
+    def __init__(self, name, template, net_href, cpus, memory,
+                 password=None, row=None, group=None):
+        self.name = name
+        self.template = template
+        self.net_href = net_href
+        self.cpus = cpus
+        self.memory = memory
+        self.password = password
+        self.row = row
+        self.group = group
+
+        self._build_xmltree()
+
+    def tostring(self):
+        return ET.tostring(self.root)
+
+    def _build_xmltree(self):
+        self.root = self._make_instantiation_root()
+
+        self._add_vapp_template(self.root)
+        instantionation_params = ET.SubElement(self.root,
+                                               "InstantiationParams")
+
+        product = self._make_product_section(instantionation_params)
+        virtual_hardware = self._make_virtual_hardware(instantionation_params)
+        network_config_section = ET.SubElement(instantionation_params,
+                                               "NetworkConfigSection")
+
+        network_config = ET.SubElement(network_config_section,
+                                       "NetworkConfig")
+        self._add_network_association(network_config)
+
+    def _make_instantiation_root(self):
+        return ET.Element(
+            "InstantiateVAppTemplateParams",
+            {'name': self.name,
+             'xml:lang': 'en',
+             'xmlns': "http://www.vmware.com/vcloud/v0.8",
+             'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"}
+        )
+
+    def _add_vapp_template(self, parent):
+        return ET.SubElement(
+            parent,
+            "VAppTemplate",
+            {'href': self.template}
+        )
+
+    def _make_product_section(self, parent):
+        prod_section = ET.SubElement(
+            parent,
+            "ProductSection",
+            {'xmlns:q1': "http://www.vmware.com/vcloud/v0.8",
+             'xmlns:ovf': "http://schemas.dmtf.org/ovf/envelope/1"}
+        )
+
+        if self.password:
+            self._add_property(prod_section, 'password', self.password)
+
+        if self.row:
+            self._add_property(prod_section, 'row', self.row)
+
+        if self.group:
+            self._add_property(prod_section, 'group', self.group)
+
+        return prod_section
+
+    def _add_property(self, parent, ovfkey, ovfvalue):
+        return ET.SubElement(
+            parent,
+            "Property",
+            {'xmlns': 'http://schemas.dmtf.org/ovf/envelope/1',
+             'ovf:key': ovfkey,
+             'ovf:value': ovfvalue}
+        )
+
+    def _make_virtual_hardware(self, parent):
+        vh = ET.SubElement(
+            parent,
+            "VirtualHardwareSection",
+            {'xmlns:q1': "http://www.vmware.com/vcloud/v0.8"}
+        )
+
+        self._add_cpu(vh)
+        self._add_memory(vh)
+
+        return vh
+
+    def _add_cpu(self, parent):
+        cpu_item = ET.SubElement(
+            parent,
+            "Item",
+            {'xmlns': "http://schemas.dmtf.org/ovf/envelope/1"}
+        )
+        self._add_instance_id(cpu_item, '1')
+        self._add_resource_type(cpu_item, '3')
+        self._add_virtual_quantity(cpu_item, self.cpus)
+
+        return cpu_item
+
+    def _add_memory(self, parent):
+        mem_item = ET.SubElement(
+            parent,
+            "Item",
+            {'xmlns': "http://schemas.dmtf.org/ovf/envelope/1"}
+        )
+        self._add_instance_id(mem_item, '2')
+        self._add_resource_type(mem_item, '4')
+        self._add_virtual_quantity(mem_item, self.memory)
+
+        return mem_item
+
+    def _add_instance_id(self, parent, id):
+        elm = ET.SubElement(
+            parent,
+            "InstanceID",
+            {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData'}
+        )
+        elm.text = id
+        return elm
+
+    def _add_resource_type(self, parent, type):
+        elm = ET.SubElement(
+            parent,
+            "ResourceType",
+            {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData'}
+        )
+        elm.text = type
+        return elm
+
+    def _add_virtual_quantity(self, parent, amount):
+       elm = ET.SubElement(
+            parent,
+            "VirtualQuantity",
+            {'xmlns': 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData'}
+        )
+       elm.text = amount
+       return elm
+
+    def _add_network_association(self, parent):
+        return ET.SubElement(
+            parent,
+            "NetworkAssociation",
+            {'href': self.net_href}
+        )
+
+class VCloudResponse(Response):
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        try:
+            return ET.XML(self.body)
+        except ExpatError, e:
+            raise Exception("%s: %s" % (e, self.parse_error()))
+
+    def parse_error(self):
+        return self.error
+
+    def success(self):
+        return self.status in (httplib.OK, httplib.CREATED,
+                               httplib.NO_CONTENT, httplib.ACCEPTED)
+
+class VCloudConnection(ConnectionUserAndKey):
+
+    responseCls = VCloudResponse
+    token = None
+    host = None
+
+    def request(self, *args, **kwargs):
+        self._get_auth_token()
+        return super(VCloudConnection, self).request(*args, **kwargs)
+
+    def check_org(self):
+        # the only way to get our org is by logging in.
+        self._get_auth_token()
+
+    def _get_auth_headers(self):
+        """Some providers need different headers than others"""
+        return {
+            'Authorization':
+                "Basic %s"
+                % base64.b64encode('%s:%s' % (self.user_id, self.key)),
+            'Content-Length': 0
+        }
+
+    def _get_auth_token(self):
+        if not self.token:
+            conn = self.conn_classes[self.secure](self.host,
+                                                  self.port[self.secure])
+            conn.request(method='POST', url='/api/v0.8/login',
+                         headers=self._get_auth_headers())
+
+            resp = conn.getresponse()
+            headers = dict(resp.getheaders())
+            body = ET.XML(resp.read())
+
+            try:
+                self.token = headers['set-cookie']
+            except KeyError:
+                raise InvalidCredsException()
+
+            self.driver.org = get_url_path(
+                body.find(fixxpath(body, 'Org')).get('href')
+            )
+
+    def add_default_headers(self, headers):
+        headers['Cookie'] = self.token
+        return headers
+
+class VCloudNodeDriver(NodeDriver):
+    type = Provider.VCLOUD
+    name = "vCloud"
+    connectionCls = VCloudConnection
+    org = None
+    _vdcs = None
+
+    NODE_STATE_MAP = {'0': NodeState.PENDING,
+                      '1': NodeState.PENDING,
+                      '2': NodeState.PENDING,
+                      '3': NodeState.PENDING,
+                      '4': NodeState.RUNNING}
+
+    @property
+    def vdcs(self):
+        if not self._vdcs:
+            self.connection.check_org() # make sure the org is set.
+            res = self.connection.request(self.org)
+            self._vdcs = [
+                get_url_path(i.get('href'))
+                for i
+                in res.object.findall(fixxpath(res.object, "Link"))
+                if i.get('type') == 'application/vnd.vmware.vcloud.vdc+xml'
+            ]
+
+        return self._vdcs
+
+    @property
+    def networks(self):
+        networks = []
+        for vdc in self.vdcs:
+            res = self.connection.request(vdc).object
+            networks.extend(
+                [network
+                 for network in res.findall(
+                     fixxpath(res, "AvailableNetworks/Network")
+                 )]
+            )
+
+        return networks
+
+    def _to_image(self, image):
+        image = NodeImage(id=image.get('href'),
+                          name=image.get('name'),
+                          driver=self.connection.driver)
+        return image
+
+    def _to_node(self, name, elm):
+        state = self.NODE_STATE_MAP[elm.get('status')]
+        public_ips = []
+        private_ips = []
+
+        # Following code to find private IPs works for Terremark
+        connections = elm.findall('{http://schemas.dmtf.org/ovf/envelope/1}NetworkConnectionSection/{http://www.vmware.com/vcloud/v0.8}NetworkConnection')
+        for connection in connections:
+          ips = [ip.text
+                 for ip
+                 in connection.findall(fixxpath(elm, "IpAddress"))]
+          if connection.get('Network') == 'Internal':
+            private_ips.extend(ips)
+          else:
+            public_ips.extend(ips)
+
+        node = Node(id=elm.get('href'),
+                    name=name,
+                    state=state,
+                    public_ip=public_ips,
+                    private_ip=private_ips,
+                    driver=self.connection.driver)
+
+        return node
+
+    def _get_catalog_hrefs(self):
+        res = self.connection.request(self.org)
+        catalogs = [
+            get_url_path(i.get('href'))
+            for i in res.object.findall(fixxpath(res.object, "Link"))
+            if i.get('type') == 'application/vnd.vmware.vcloud.catalog+xml'
+        ]
+
+        return catalogs
+
+    def _wait_for_task_completion(self, task_href,
+                                  timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
+        start_time = time.time()
+        res = self.connection.request(task_href)
+        status = res.object.get('status')
+        while status != 'success':
+          if status == 'error':
+              raise Exception("Error status returned by task %s."
+                              % task_href)
+          if status == 'canceled':
+              raise Exception("Canceled status returned by task %s."
+                              % task_href)
+          if (time.time() - start_time >= timeout):
+              raise Exception("Timeout while waiting for task %s."
+                              % task_href)
+          time.sleep(5)
+          res = self.connection.request(task_href)
+          status = res.object.get('status')
+
+    def destroy_node(self, node):
+        node_path = get_url_path(node.id)
+        # blindly poweroff node, it will throw an exception if already off
+        try:
+            res = self.connection.request('%s/power/action/poweroff'
+                                          % node_path,
+                                          method='POST')
+            self._wait_for_task_completion(res.object.get('href'))
+        except Exception, e:
+            pass
+
+        try:
+            res = self.connection.request('%s/action/undeploy' % node_path,
+                                          method='POST')
+            self._wait_for_task_completion(res.object.get('href'))
+        except ExpatError:
+            # The undeploy response is malformed XML atm.
+            # We can remove this whent he providers fix the problem.
+            pass
+        except Exception, e:
+            # Some vendors don't implement undeploy at all yet,
+            # so catch this and move on.
+            pass
+
+        res = self.connection.request(node_path, method='DELETE')
+        return res.status == 202
+
+    def reboot_node(self, node):
+        res = self.connection.request('%s/power/action/reset'
+                                      % get_url_path(node.id),
+                                      method='POST')
+        return res.status == 202 or res.status == 204
+
+    def list_nodes(self):
+        nodes = []
+        for vdc in self.vdcs:
+            res = self.connection.request(vdc)
+            elms = res.object.findall(fixxpath(
+                res.object, "ResourceEntities/ResourceEntity")
+            )
+            vapps = [
+                (i.get('name'), get_url_path(i.get('href')))
+                for i in elms
+                if i.get('type')
+                    == 'application/vnd.vmware.vcloud.vApp+xml'
+                    and i.get('name')
+            ]
+
+            for vapp_name, vapp_href in vapps:
+                res = self.connection.request(
+                    vapp_href,
+                    headers={
+                        'Content-Type':
+                            'application/vnd.vmware.vcloud.vApp+xml'
+                    }
+                )
+                nodes.append(self._to_node(vapp_name, res.object))
+
+        return nodes
+
+    def _to_size(self, ram):
+        ns = NodeSize(
+            id=None,
+            name="%s Ram" % ram,
+            ram=ram,
+            disk=None,
+            bandwidth=None,
+            price=None,
+            driver=self.connection.driver
+        )
+        return ns
+
+    def list_sizes(self, location=None):
+        sizes = [self._to_size(i) for i in VIRTUAL_MEMORY_VALS]
+        return sizes
+
+    def _get_catalogitems_hrefs(self, catalog):
+        """Given a catalog href returns contained catalog item hrefs"""
+        res = self.connection.request(
+            catalog,
+            headers={
+                'Content-Type':
+                    'application/vnd.vmware.vcloud.catalog+xml'
+            }
+        ).object
+
+        cat_items = res.findall(fixxpath(res, "CatalogItems/CatalogItem"))
+        cat_item_hrefs = [i.get('href')
+                          for i in cat_items
+                          if i.get('type') ==
+                              'application/vnd.vmware.vcloud.catalogItem+xml']
+
+        return cat_item_hrefs
+
+    def _get_catalogitem(self, catalog_item):
+        """Given a catalog item href returns elementree"""
+        res = self.connection.request(
+            catalog_item,
+            headers={
+                'Content-Type':
+                    'application/vnd.vmware.vcloud.catalogItem+xml'
+            }
+        ).object
+
+        return res
+
+    def list_images(self, location=None):
+        images = []
+        for vdc in self.vdcs:
+            res = self.connection.request(vdc).object
+            res_ents = res.findall(fixxpath(
+                res, "ResourceEntities/ResourceEntity")
+            )
+            images += [
+                self._to_image(i)
+                for i in res_ents
+                if i.get('type') ==
+                    'application/vnd.vmware.vcloud.vAppTemplate+xml'
+            ]
+
+        for catalog in self._get_catalog_hrefs():
+            for cat_item in self._get_catalogitems_hrefs(catalog):
+                res = self._get_catalogitem(cat_item)
+                res_ents = res.findall(fixxpath(res, 'Entity'))
+                images += [
+                    self._to_image(i)
+                    for i in res_ents
+                    if i.get('type') ==
+                        'application/vnd.vmware.vcloud.vAppTemplate+xml'
+                ]
+
+        return images
+
+    def create_node(self, **kwargs):
+        """Creates and returns node.
+
+        Non-standard optional keyword arguments:
+        network -- link to a "Network" e.g.,
+            "https://services.vcloudexpress.terremark.com/api/v0.8/network/7"
+        vdc -- link to a "VDC" e.g.,
+            "https://services.vcloudexpress.terremark.com/api/v0.8/vdc/1"
+        cpus -- number of virtual cpus (limit depends on provider)
+        password
+        row
+        group
+        """
+        name = kwargs['name']
+        image = kwargs['image']
+        size = kwargs['size']
+
+        # Some providers don't require a network link
+        try:
+            network = kwargs.get('network', self.networks[0].get('href'))
+        except IndexError:
+            network = ''
+
+        password = None
+        if kwargs.has_key('auth'):
+            auth = kwargs['auth']
+            if isinstance(auth, NodeAuthPassword):
+                password = auth.password
+            else:
+                raise ValueError('auth must be of NodeAuthPassword type')
+
+        instantiate_xml = InstantiateVAppXML(
+            name=name,
+            template=image.id,
+            net_href=network,
+            cpus=str(kwargs.get('cpus', 1)),
+            memory=str(size.ram),
+            password=password,
+            row=kwargs.get('row', None),
+            group=kwargs.get('group', None)
+        )
+
+        # Instantiate VM and get identifier.
+        res = self.connection.request(
+            '%s/action/instantiateVAppTemplate'
+                % kwargs.get('vdc', self.vdcs[0]),
+            data=instantiate_xml.tostring(),
+            method='POST',
+            headers={
+                'Content-Type':
+                    'application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml'
+            }
+        )
+        vapp_name = res.object.get('name')
+        vapp_href = get_url_path(res.object.get('href'))
+
+        # Deploy the VM from the identifier.
+        res = self.connection.request('%s/action/deploy' % vapp_href,
+                                      method='POST')
+
+        self._wait_for_task_completion(res.object.get('href'))
+
+        # Power on the VM.
+        res = self.connection.request('%s/power/action/powerOn' % vapp_href,
+                                      method='POST')
+
+        res = self.connection.request(vapp_href)
+        node = self._to_node(vapp_name, res.object)
+
+        return node
+
+    features = {"create_node": ["password"]}
+
+class HostingComConnection(VCloudConnection):
+    host = "vcloud.safesecureweb.com"
+
+    def _get_auth_headers(self):
+        """hosting.com doesn't follow the standard vCloud authentication API"""
+        return {
+            'Authentication':
+                base64.b64encode('%s:%s' % (self.user_id, self.key)),
+            'Content-Length': 0
+        }
+
+class HostingComDriver(VCloudNodeDriver):
+    connectionCls = HostingComConnection
+
+class TerremarkConnection(VCloudConnection):
+    host = "services.vcloudexpress.terremark.com"
+
+class TerremarkDriver(VCloudNodeDriver):
+    connectionCls = TerremarkConnection
+    def list_locations(self):
+        return [NodeLocation(0, "Terremark Texas", 'US', self)]
diff --git a/libcloud/drivers/voxel.py b/libcloud/drivers/voxel.py
new file mode 100644
index 0000000..dcd70a1
--- /dev/null
+++ b/libcloud/drivers/voxel.py
@@ -0,0 +1,244 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Voxel VoxCloud driver
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+import datetime
+import hashlib
+from xml.etree import ElementTree as ET
+
+VOXEL_API_HOST = "api.voxel.net"
+
+class VoxelResponse(Response):
+
+    def __init__(self, response):
+        self.parsed = None
+        super(VoxelResponse, self).__init__(response)
+
+    def parse_body(self):
+        if not self.body:
+            return None
+        if not self.parsed:
+            self.parsed = ET.XML(self.body)
+        return self.parsed
+
+    def parse_error(self):
+        err_list = []
+        if not self.body:
+            return None
+        if not self.parsed:
+            self.parsed = ET.XML(self.body)
+        for err in self.parsed.findall('err'):
+            code = err.get('code')
+            err_list.append("(%s) %s" % (code, err.get('msg')))
+            # From voxel docs:
+            # 1: Invalid login or password
+            # 9: Permission denied: user lacks access rights for this method
+            if code == "1" or code == "9":
+                # sucks, but only way to detect
+                # bad authentication tokens so far
+                raise InvalidCredsException(err_list[-1])
+        return "\n".join(err_list)
+
+    def success(self):
+        if not self.parsed:
+            self.parsed = ET.XML(self.body)
+        stat = self.parsed.get('stat')
+        if stat != "ok":
+            return False
+        return True
+
+class VoxelConnection(ConnectionUserAndKey):
+
+    host = VOXEL_API_HOST
+    responseCls = VoxelResponse
+
+    def add_default_params(self, params):
+        params["key"] = self.user_id
+        params["timestamp"] = datetime.datetime.utcnow().isoformat()+"+0000"
+
+        for param in params.keys():
+            if params[param] is None:
+                del params[param]
+
+        keys = params.keys()
+        keys.sort()
+
+        md5 = hashlib.md5()
+        md5.update(self.key)
+        for key in keys:
+            if params[key]:
+                if not params[key] is None:
+                    md5.update("%s%s"% (key, params[key]))
+                else:
+                    md5.update(key)
+        params['api_sig'] = md5.hexdigest()
+        return params
+
+VOXEL_INSTANCE_TYPES = {}
+RAM_PER_CPU = 2048
+
+NODE_STATE_MAP = { 'IN_PROGRESS': NodeState.PENDING,
+                   'SUCCEEDED': NodeState.RUNNING,
+                   'shutting-down': NodeState.TERMINATED,
+                   'terminated': NodeState.TERMINATED }
+
+class VoxelNodeDriver(NodeDriver):
+
+    connectionCls = VoxelConnection
+    type = Provider.VOXEL
+    name = 'Voxel VoxCLOUD'
+
+    def initialize_instance_types():
+        for cpus in range(1,14):
+            if cpus == 1:
+                name = "Single CPU"
+            else:
+                name = "%d CPUs" % cpus
+            id = "%dcpu" % cpus
+            ram = cpus * RAM_PER_CPU
+
+            VOXEL_INSTANCE_TYPES[id]= {
+                         'id': id,
+                         'name': name,
+                         'ram': ram,
+                         'disk': None,
+                         'bandwidth': None,
+                         'price': None}
+
+    features = {"create_node": [],
+                "list_sizes":  ["variable_disk"]}
+
+    initialize_instance_types()
+
+    def list_nodes(self):
+        params = {"method": "voxel.devices.list"}
+        result = self.connection.request('/', params=params).object
+        return self._to_nodes(result)
+
+    def list_sizes(self, location=None):
+        return [ NodeSize(driver=self.connection.driver, **i)
+                    for i in VOXEL_INSTANCE_TYPES.values() ]
+
+    def list_images(self, location=None):
+        params = {"method": "voxel.images.list"}
+        result = self.connection.request('/', params=params).object
+        return self._to_images(result)
+
+    def create_node(self, **kwargs):
+        raise NotImplementedError, \
+            'create_node not finished for voxel yet'
+        size = kwargs["size"]
+        cores = size.ram / RAM_PER_CPU
+        params = {'method':           'voxel.voxcloud.create',
+                  'hostname':         kwargs["name"],
+                  'disk_size':        int(kwargs["disk"])/1024,
+                  'processing_cores': cores,
+                  'facility':         kwargs["location"].id,
+                  'image_id':         kwargs["image"],
+                  'backend_ip':       kwargs.get("privateip", None),
+                  'frontend_ip':      kwargs.get("publicip", None),
+                  'admin_password':   kwargs.get("rootpass", None),
+                  'console_password': kwargs.get("consolepass", None),
+                  'ssh_username':     kwargs.get("sshuser", None),
+                  'ssh_password':     kwargs.get("sshpass", None),
+                  'voxel_access':     kwargs.get("voxel_access", None)}
+
+        object = self.connection.request('/', params=params).object
+
+        if self._getstatus(object):
+            return Node(
+                id = object.findtext("device/id"),
+                name = kwargs["name"],
+                state = NODE_STATE_MAP[object.findtext("devices/status")],
+                public_ip = public_ip,
+                private_ip = private_ip,
+                driver = self.connection.driver
+            )
+        else:
+            return None
+
+    def reboot_node(self, node):
+        """
+        Reboot the node by passing in the node object
+        """
+        params = {'method': 'voxel.devices.power',
+                  'device_id': node.id,
+                  'power_action': 'reboot'}
+        return self._getstatus(self.connection.request('/', params=params).object)
+
+    def destroy_node(self, node):
+        """
+        Destroy node by passing in the node object
+        """
+        params = {'method': 'voxel.voxcloud.delete',
+                  'device_id': node.id}
+        return self._getstatus(self.connection.request('/', params=params).object)
+
+    def list_locations(self):
+        params = {"method": "voxel.voxcloud.facilities.list"}
+        result = self.connection.request('/', params=params).object
+        nodes = self._to_locations(result)
+        return nodes
+
+    def _getstatus(self, element):
+        status = element.attrib["stat"]
+        return status == "ok"
+
+
+    def _to_locations(self, object):
+        return [NodeLocation(element.attrib["label"],
+                             element.findtext("description"),
+                             element.findtext("description"),
+                             self)
+                for element in object.findall('facilities/facility')]
+
+    def _to_nodes(self, object):
+        nodes = []
+        for element in object.findall('devices/device'):
+            if element.findtext("type") == "Virtual Server":
+                try:
+                    state = self.NODE_STATE_MAP[element.attrib['status']]
+                except KeyError:
+                    state = NodeState.UNKNOWN
+
+                public_ip = private_ip = None
+                ipassignments = element.findall("ipassignments/ipassignment")
+                for ip in ipassignments:
+                    if ip.attrib["type"] =="frontend":
+                        public_ip = ip.text
+                    elif ip.attrib["type"] == "backend":
+                        private_ip = ip.text
+
+                nodes.append(Node(id= element.attrib['id'],
+                                 name=element.attrib['label'],
+                                 state=state,
+                                 public_ip= public_ip,
+                                 private_ip= private_ip,
+                                 driver=self.connection.driver))
+        return nodes
+
+    def _to_images(self, object):
+        images = []
+        for element in object.findall("images/image"):
+            images.append(NodeImage(id = element.attrib["id"],
+                                    name = element.attrib["summary"],
+                                    driver = self.connection.driver))
+        return images
diff --git a/libcloud/drivers/vpsnet.py b/libcloud/drivers/vpsnet.py
new file mode 100644
index 0000000..e7adb19
--- /dev/null
+++ b/libcloud/drivers/vpsnet.py
@@ -0,0 +1,181 @@
+# 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.
+# libcloud.org 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.
+"""
+VPS.net driver
+"""
+from libcloud.providers import Provider
+from libcloud.types import NodeState, InvalidCredsException
+from libcloud.base import Node, Response, ConnectionUserAndKey, NodeDriver
+from libcloud.base import NodeSize, NodeImage, NodeLocation
+
+import base64
+
+# JSON is included in the standard library starting with Python 2.6.  For 2.5
+# and 2.4, there's a simplejson egg at: http://pypi.python.org/pypi/simplejson
+try: import json
+except: import simplejson as json
+
+API_HOST = 'api.vps.net'
+API_VERSION = 'api10json'
+
+RAM_PER_NODE = 256
+DISK_PER_NODE = 10
+BANDWIDTH_PER_NODE = 250
+PRICE_PER_NODE = {1: 20,
+                  2: 19,
+                  3: 18,
+                  4: 17,
+                  5: 16,
+                  6: 15,
+                  7: 14,
+                  15: 13,
+                  30: 12,
+                  60: 11,
+                  100: 10}
+
+class VPSNetResponse(Response):
+    
+    def parse_body(self):
+        try:
+            js = json.loads(self.body)
+            return js
+        except ValueError:
+            return self.body
+
+    def success(self):
+        # vps.net wrongly uses 406 for invalid auth creds
+        if self.status == 406 or self.status == 403:
+          raise InvalidCredsException()
+        return True
+
+    def parse_error(self):
+        try:
+            errors = json.loads(self.body)['errors'][0]
+        except ValueError:
+            return self.body
+        else:
+            return "\n".join(errors)
+
+class VPSNetConnection(ConnectionUserAndKey):
+
+    host = API_HOST
+    responseCls = VPSNetResponse
+
+    def add_default_headers(self, headers):
+        user_b64 = base64.b64encode('%s:%s' % (self.user_id, self.key))
+        headers['Authorization'] = 'Basic %s' % (user_b64)
+        return headers
+
+class VPSNetNodeDriver(NodeDriver):
+    
+    type = Provider.VPSNET
+    name = "vps.net"
+    connectionCls = VPSNetConnection
+
+    def _to_node(self, vm):
+        if vm['running']:
+            state = NodeState.RUNNING
+        else:
+            state = NodeState.PENDING
+
+        n = Node(id=vm['id'],
+                 name=vm['label'],
+                 state=state,
+                 public_ip=[vm.get('primary_ip_address', None)],
+                 private_ip=[],
+                 driver=self.connection.driver)
+        return n
+
+    def _to_image(self, image, cloud):
+        image = NodeImage(id=image['id'],
+                          name="%s: %s" % (cloud, image['label']),
+                          driver=self.connection.driver)
+
+        return image
+
+    def _to_size(self, num):
+        size = NodeSize(id=num,
+                        name="%d Node" % (num,),
+                        ram=RAM_PER_NODE * num,
+                        disk=DISK_PER_NODE,
+                        bandwidth=BANDWIDTH_PER_NODE * num,
+                        price=self._get_price_per_node(num) * num,
+                        driver=self.connection.driver)
+        return size
+
+    def _get_price_per_node(self, num):
+        keys = sorted(PRICE_PER_NODE.keys())
+
+        if num >= max(keys):
+            return PRICE_PER_NODE[keys[-1]]
+
+        for i in range(0,len(keys)):
+            if keys[i] <= num < keys[i+1]:
+                return PRICE_PER_NODE[keys[i]]
+
+    def create_node(self, name, image, size, **kwargs):
+        headers = {'Content-Type': 'application/json'}
+        request = {'virtual_machine':
+                        {'label': name,
+                         'fqdn': kwargs.get('fqdn', ''),
+                         'system_template_id': image.id,
+                         'backups_enabled': kwargs.get('backups_enabled', 0),
+                         'slices_required': size.id}}
+
+        res = self.connection.request('/virtual_machines.%s' % (API_VERSION,),
+                                    data=json.dumps(request),
+                                    headers=headers,
+                                    method='POST')
+        node = self._to_node(res.object['virtual_machine'])
+        return node
+
+    def reboot_node(self, node):
+        res = self.connection.request('/virtual_machines/%s/%s.%s' % 
+                                        (node.id, 'reboot', API_VERSION),
+                                        method="POST")
+        node = self._to_node(res.object['virtual_machine'])
+        return True
+    
+    def list_sizes(self, location=None):
+        res = self.connection.request('/nodes.%s' % (API_VERSION,))
+        available_nodes = len([size for size in res.object 
+                            if not size['slice']["virtual_machine_id"]])
+        sizes = [self._to_size(i) for i in range(1,available_nodes + 1)]
+        return sizes
+
+    def destroy_node(self, node):
+        res = self.connection.request('/virtual_machines/%s.%s'
+                                      % (node.id, API_VERSION),
+                                      method='DELETE')
+        return res.status == 200
+
+    def list_nodes(self):
+        res = self.connection.request('/virtual_machines.%s' % (API_VERSION,))
+        return [self._to_node(i['virtual_machine']) for i in res.object] 
+
+    def list_images(self, location=None):
+        res = self.connection.request('/available_clouds.%s' % (API_VERSION,))
+
+        images = []
+        for cloud in res.object:
+            label = cloud['cloud']['label']
+            templates = cloud['cloud']['system_templates']
+            images.extend([self._to_image(image, label)
+                           for image in templates])
+
+        return images
+
+    def list_locations(self):
+        return [NodeLocation(0, "VPS.net Western US", 'US', self)]
diff --git a/libcloud/interface.py b/libcloud/interface.py
new file mode 100644
index 0000000..6137564
--- /dev/null
+++ b/libcloud/interface.py
@@ -0,0 +1,328 @@
+# 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.
+# libcloud.org 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.
+
+"""
+Provides zope.interface definitions for libcloud.
+"""
+from zope.interface import Interface, Attribute
+
+
+class INode(Interface):
+    """
+    A node (instance, etc)
+    """
+    uuid = Attribute("""Unique identifier""")
+    id = Attribute("""Unique ID provided by the provider (i-abcd1234, etc)""")
+    name = Attribute("""Hostname or similar identifier""")
+    state = Attribute("""A standard Node state as provided by L{NodeState}""")
+    public_ip = Attribute("""List of Public IPs of the Node""")
+    private_ip = Attribute("""List of Private IPs of the Node""")
+    driver = Attribute("""The NodeDriver that belongs to this Node""")
+    extra = Attribute("""Dict containing provider specific data""")
+
+    def get_uuid():
+        """
+        Provides a system wide unique ID for the node
+        """
+    def destroy():
+        """
+        Call `self.driver.destroy_node(self)`. A convenience method.
+        """
+
+    def reboot():
+        """
+        Call `self.driver.reboot_node(self)`. A convenience method.
+        """
+
+
+class INodeFactory(Interface):
+    """
+    Create nodes
+    """
+    def __call__(id, name, state, public_ip, private_ip, driver):
+        """
+        Set values for ivars, including any other requisite kwargs
+        """
+
+
+class INodeSize(Interface):
+    """
+    A machine image
+    """
+    id = Attribute("""Unique ID provided by the provider (m1.small, etc)""")
+    name = Attribute("""Name provided by the provider (Small CPU, etc)""")
+    ram = Attribute("""Amount of RAM provided in MB (256MB, 1740MB)""")
+    disk = Attribute("""Amount of disk provided in GB (200GB)""")
+    bandwidth = Attribute("""Amount of total transfer bandwidth in GB""")
+    price = Attribute("""Hourly price of this server in USD, estimated if
+                         monthly""")
+    driver = Attribute("""The NodeDriver that belongs to this Image""")
+
+
+class INodeSizeFactory(Interface):
+    """
+    Create nodes
+    """
+    def __call__(id, name, ram, disk, bandwidth, price, driver):
+        """
+        Set values for ivars, including any other requisite kwargs
+        """
+
+
+class INodeImage(Interface):
+    """
+    A machine image
+    """
+    id = Attribute("""Unique ID provided by the provider (ami-abcd1234)""")
+    name = Attribute("""Name provided by the provider (Ubuntu 8.1)""")
+    driver = Attribute("""The NodeDriver that belongs to this Image""")
+    extra = Attribute("""Dict containing provider specific data""")
+
+class INodeImageFactory(Interface):
+    """
+    Create nodes
+    """
+    def __call__(id, name, driver):
+        """
+        Set values for ivars, including any other requisite kwargs
+        """
+
+class INodeLocation(Interface):
+    """
+    Physical Location of a node
+    """
+    id = Attribute("""Unique ID provided by the provider for a physical
+                      datacenter""")
+    name = Attribute("""Name provided by the provider ('Austin Texas DC 1')""")
+    country = Attribute("""ISO 3166 country code of the physical location of
+                           the data center <http://bit.ly/pKie5> (iso.org)""")
+    driver = Attribute("""The NodeDriver that belongs to this Location""")
+
+class INodeLocationFactory(Interface):
+    """
+    Create nodes location
+    """
+    def __call__(id, name, country, driver):
+        """
+        Set values for ivars, including any other requisite kwargs
+        """
+
+class INodeDriverFactory(Interface):
+    """
+    Create NodeDrivers
+    """
+    def __call__(key, secret=None, secure=True):
+        """
+        Set of value for ivars
+        """
+
+
+class INodeDriver(Interface):
+    """
+    A driver which provides nodes, such as an Amazon EC2 instance,
+    or Slicehost slice
+    """
+
+    connection = Attribute("""Represents the IConnection for this driver""")
+    type = Attribute("""The type of this provider as defined by L{Provider}""")
+    name = Attribute("""A pretty name (Linode, etc) for this provider""")
+
+    NODE_STATE_MAP = Attribute("""A mapping of states found in the response to
+                              their standard type. This is a constant.""")
+
+    def create_node(**kwargs):
+        """
+        Creates a new node based on provided params. Name is ignored on
+        some providers.
+
+        To specify provider-specific options, use keyword arguments.
+        """
+
+    def destroy_node(node):
+        """
+        Returns True if the destroy was successful, otherwise False
+        """
+
+    def list_nodes():
+        """
+        Returns a list of nodes for this provider
+        """
+
+    def list_images(location=None):
+        """
+        Returns a list of images for this provider
+        """
+
+    def list_sizes(location=None):
+        """
+        Returns a list of sizes for this provider
+        """
+
+    def list_locations():
+        """
+        Returns a list of locations for this prodiver
+        """
+
+    def reboot_node(node):
+        """
+        Returns True if the reboot was successful, otherwise False
+        """
+
+class IConnection(Interface):
+    """
+    A Connection represents an interface between a Client and a Provider's Web
+    Service. It is capable of authenticating, making requests, and returning
+    responses.
+    """
+    conn_classes = Attribute("""Classes used to create connections, should be
+                            in the form of `(insecure, secure)`""")
+    responseCls = Attribute("""Provider-specific Class used for creating
+                           responses""")
+    connection = Attribute("""Represents the lower-level connection to the
+                          server""")
+    host = Attribute("""Default host for this connection""")
+    port = Attribute("""Default port for this connection. This should be a
+                    tuple of the form `(insecure, secure)` or for single-port
+                    Providers, simply `(port,)`""")
+    secure = Attribute("""Indicates if this is a secure connection. If previous
+                      recommendations were followed, it would be advantageous
+                      for this to be in the form: 0=insecure, 1=secure""")
+    driver = Attribute("""The NodeDriver that belongs to this Node""")
+
+    def connect(host=None, port=None):
+        """
+        A method for establishing a connection. If no host or port are given,
+        existing ivars should be used.
+        """
+
+    def request(action, params={}, data='', method='GET'):
+        """
+        Make a request.
+
+        An `action` should represent a path, such as `/list/nodes`. Query
+        parameters necessary to the request should be passed in `params` and
+        any data to encode goes in `data`. `method` should be one of: (GET,
+        POST).
+
+        Should return a response object (specific to a provider).
+        """
+
+    def add_default_params(params):
+        """
+        Adds default parameters (such as API key, version, etc.)
+        to the passed `params`
+
+        Should return a dictionary.
+        """
+
+    def add_default_headers(headers):
+        """
+        Adds default headers (such as Authorization, X-Foo-Bar)
+        to the passed `headers`
+
+        Should return a dictionary.
+        """
+
+    def encode_data(data):
+        """
+        Data may need to be encoded before sent in a request.
+        If not, simply return the data.
+        """
+
+
+class IConnectionKey(IConnection):
+    """
+    IConnection which only depends on an API key for authentication.
+    """
+    key = Attribute("""API key, token, etc.""")
+
+
+class IConnectionUserAndKey(IConnectionKey):
+    """
+    IConnection which depends on a user identifier and an API
+    for authentication.
+    """
+    user_id = Attribute("""User identifier""")
+
+
+class IConnectionKeyFactory(Interface):
+    """
+    Create Connections which depend solely on an API key.
+    """
+    def __call__(key, secure=True):
+        """
+        Create a Connection.
+
+        The acceptance of only `key` provides support for APIs with only one
+        authentication bit.
+        
+        The `secure` argument indicates whether or not a secure connection
+        should be made. Not all providers support this, so it may be ignored.
+        """
+
+
+class IConnectionUserAndKeyFactory(Interface):
+    """
+    Create Connections which depends on both a user identifier and API key.
+    """
+    def __call__(user_id, key, secure=True):
+        """
+        Create a Connection.
+
+        The first two arguments provide the initial values for `user_id` and
+        `key`, respectively, which should be used for authentication.
+        
+        The `secure` argument indicates whether or not a secure connection
+        should be made. Not all providers support this, so it may be ignored.
+        """
+
+
+class IResponse(Interface):
+    """
+    A response as provided by a given HTTP Client.
+    """
+    object = Attribute("""The processed response object,
+                          e.g. via lxml or json""")
+    body = Attribute("""Unparsed response body""")
+    status = Attribute("""Response status code""")
+    headers = Attribute("""Response headers""")
+    error = Attribute("""Response error, L{None} if no error.""")
+    connection = Attribute("""Represents the IConnection for this response""")
+
+    def parse_body():
+        """
+        Parse the response body (as XML, etc.)
+        """
+
+    def parse_error():
+        """
+        Parse the error that is contained in the response body (as XML, etc.)
+        """
+
+    def success():
+        """
+        Does the response indicate a successful request?
+        """
+
+
+class IResponseFactory(Interface):
+    """
+    Creates Responses.
+    """
+    def __call__(response):
+        """
+        Process the given response, setting ivars.
+        """
+
diff --git a/libcloud/providers.py b/libcloud/providers.py
new file mode 100644
index 0000000..b46d10f
--- /dev/null
+++ b/libcloud/providers.py
@@ -0,0 +1,54 @@
+# 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.
+# libcloud.org 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.
+"""
+Provider related utilities
+"""
+
+from libcloud.types import Provider
+
+DRIVERS = {
+    Provider.DUMMY:
+        ('libcloud.drivers.dummy', 'DummyNodeDriver'),
+    Provider.EC2_US_EAST:
+        ('libcloud.drivers.ec2', 'EC2NodeDriver'),
+    Provider.EC2_EU_WEST:
+        ('libcloud.drivers.ec2', 'EC2EUNodeDriver'),
+    Provider.EC2_US_WEST:
+        ('libcloud.drivers.ec2', 'EC2USWestNodeDriver'),
+    Provider.GOGRID:
+        ('libcloud.drivers.gogrid', 'GoGridNodeDriver'),
+    Provider.RACKSPACE:
+        ('libcloud.drivers.rackspace', 'RackspaceNodeDriver'),
+    Provider.SLICEHOST:
+        ('libcloud.drivers.slicehost', 'SlicehostNodeDriver'),
+    Provider.VPSNET:
+        ('libcloud.drivers.vpsnet', 'VPSNetNodeDriver'),
+    Provider.LINODE:
+        ('libcloud.drivers.linode', 'LinodeNodeDriver'),
+    Provider.RIMUHOSTING:
+        ('libcloud.drivers.rimuhosting', 'RimuHostingNodeDriver'),
+    Provider.VOXEL:
+        ('libcloud.drivers.voxel', 'VoxelNodeDriver'),	
+}
+
+def get_driver(provider):
+    """Gets a driver
+    @param provider: Id of provider to get driver
+    @type provider: L{libcloud.types.Provider}
+    """
+    if provider in DRIVERS:
+        mod_name, driver_name = DRIVERS[provider]
+        _mod = __import__(mod_name, globals(), locals(), [driver_name])
+        return getattr(_mod, driver_name)
diff --git a/libcloud/types.py b/libcloud/types.py
new file mode 100644
index 0000000..adc9322
--- /dev/null
+++ b/libcloud/types.py
@@ -0,0 +1,71 @@
+# 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.
+# libcloud.org 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.
+"""
+Base types used by other parts of libcloud
+"""
+
+class Provider(object):
+    """
+    Defines for each of the supported providers
+
+    @cvar DUMMY: Example provider
+    @cvar EC2_US_EAST: Amazon AWS US N. Virgina
+    @cvar EC2_US_WEST: Amazon AWS US N. California
+    @cvar EC2_EU_WEST: Amazon AWS EU Ireland
+    @cvar RACKSPACE: Rackspace Cloud Servers
+    @cvar SLICEHOST: Slicehost.com
+    @cvar GOGRID: GoGrid
+    @cvar VPSNET: VPS.net
+    @cvar LINODE: Linode.com
+    @cvar VCLOUD: vmware vCloud
+    @cvar RIMUHOSTING: RimuHosting.com
+    """
+    DUMMY = 0
+    EC2 = 1  # deprecated name
+    EC2_US_EAST = 1
+    EC2_EU = 2 # deprecated name
+    EC2_EU_WEST = 2
+    RACKSPACE = 3
+    SLICEHOST = 4
+    GOGRID = 5
+    VPSNET = 6
+    LINODE = 7
+    VCLOUD = 8
+    RIMUHOSTING = 9
+    EC2_US_WEST = 10
+    VOXEL = 11
+
+class NodeState(object):
+    """
+    Standard states for a node
+
+    @cvar RUNNING: Node is running
+    @cvar REBOOTING: Node is rebooting
+    @cvar TERMINATED: Node is terminated
+    @cvar PENDING: Node is pending
+    @cvar UNKNOWN: Node state is unknown
+    """
+    RUNNING = 0
+    REBOOTING = 1
+    TERMINATED = 2
+    PENDING = 3
+    UNKNOWN = 4
+
+class InvalidCredsException(Exception):
+    """Exception used when invalid credentials are used on a provider."""
+    def __init__(self, value='Invalid credentials with the provider'):
+        self.value = value
+    def __str__(self):
+        return repr(self.value)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..6ebaa5c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,100 @@
+# 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.
+# libcloud.org 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
+from distutils.core import setup
+from distutils.core import Command
+from unittest import TextTestRunner, TestLoader
+from glob import glob
+from os.path import splitext, basename, join as pjoin
+
+HTML_VIEWSOURCE_BASE = 'https://svn.apache.org/viewvc/incubator/libcloud/trunk'
+PROJECT_BASE_DIR = 'http://incubator.apache.org/libcloud/'
+
+class TestCommand(Command):
+    user_options = []
+
+    def initialize_options(self):
+        THIS_DIR = os.path.abspath(os.path.split(__file__)[0])
+        sys.path.insert(0, THIS_DIR)
+        sys.path.insert(0, pjoin(THIS_DIR, 'test'))
+        self._dir = os.getcwd()
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        testfiles = []
+        for t in glob(pjoin(self._dir, 'test', 'test_*.py')):
+            testfiles.append('.'.join(
+                ['test', splitext(basename(t))[0]])
+            )
+
+        tests = TestLoader().loadTestsFromNames(testfiles)
+        t = TextTestRunner(verbosity = 1)
+        res = t.run(tests)
+        sys.exit(not res.wasSuccessful())
+
+class ApiDocsCommand(Command):
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        os.system(
+            'pydoctor'
+            ' --add-package=libcloud'
+            ' --project-name=libcloud'
+            ' --make-html'
+            ' --html-viewsource-base="%s"'
+            ' --project-base-dir=`pwd`'
+            ' --project-url="%s"'
+            % (HTML_VIEWSOURCE_BASE, PROJECT_BASE_DIR)
+        )
+
+setup(
+    name='apache-libcloud',
+    version='0.2.1',
+    description='A unified interface into many cloud server providers',
+    author='Apache Software Foundation',
+    author_email='libcloud@incubator.apache.org',
+    packages=[
+        'libcloud',
+        'libcloud.drivers'
+    ],
+    package_dir={
+        'libcloud': 'libcloud',
+        'libcloud.drivers': 'libcloud/drivers'
+    },
+    license='Apache License (2.0)',
+    url='http://incubator.apache.org/libcloud/',
+    cmdclass={
+        'test': TestCommand,
+        'apidocs': ApiDocsCommand
+    },
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Environment :: Console',
+        'Intended Audience :: System Administrators',
+        'License :: OSI Approved :: Apache Software License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Python Modules'
+    ],
+)
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..04072ab
--- /dev/null
+++ b/test/__init__.py
@@ -0,0 +1,208 @@
+# 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.
+# libcloud.org 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 httplib
+from cStringIO import StringIO
+from urllib2 import urlparse
+from cgi import parse_qs
+from libcloud.base import Node, NodeImage, NodeSize, NodeLocation
+from libcloud.types import NodeState
+import unittest
+
+class multipleresponse(object):
+    """
+    A decorator that allows MockHttp objects to return multi responses
+    """
+    count = 0
+    func = None
+
+    def __init__(self, f):
+        self.func = f
+
+    def __call__(self, *args, **kwargs):
+        ret = self.func(self.func.__class__, *args, **kwargs)
+        response = ret[self.count]
+        self.count = self.count + 1
+        return response
+
+
+class MockResponse(object):
+    """
+    A mock HTTPResponse
+    """
+    headers = {}
+    body = StringIO()
+    status = 0
+    reason = ''
+    version = 11
+
+    def __init__(self, status, body, headers=None, reason=None):
+        self.status = status
+        self.body = StringIO(body)
+        self.headers = headers or self.headers
+        self.reason = reason or self.reason
+
+    def read(self, *args, **kwargs):
+        return self.body.read(*args, **kwargs)
+    
+    def getheader(self, name, *args, **kwargs):
+        return self.headers.get(name, *args, **kwargs)
+    
+    def getheaders(self):
+        return self.headers.items()
+
+    def msg(self):
+        raise NotImplemented
+
+
+class MockHttp(object):
+    """
+    A mock HTTP client/server suitable for testing purposes. This replaces 
+    `HTTPConnection` by implementing its API and returning a mock response.
+
+    Define methods by request path, replacing slashes (/) with underscores (_).
+    Each of these mock methods should return a tuple of:
+        
+        (int status, str body, dict headers, str reason)
+
+    >>> mock = MockHttp('localhost', 8080)
+    >>> mock.request('GET', '/example/')
+    >>> response = mock.getresponse()
+    >>> response.body.read()
+    'Hello World!'
+    >>> response.status
+    200
+    >>> response.getheaders()
+    [('X-Foo', 'libcloud')]
+    >>> MockHttp.type = 'fail'
+    >>> mock.request('GET', '/example/')
+    >>> response = mock.getresponse()
+    >>> response.body.read()
+    'Oh Noes!'
+    >>> response.status
+    403
+    >>> response.getheaders()
+    [('X-Foo', 'fail')]
+
+    """
+    responseCls = MockResponse
+    host = None
+    port = None
+    response = None
+
+    type = None 
+    use_param = None # will use this param to namespace the request function
+
+    def __init__(self, host, port, *args, **kwargs):
+        self.host = host
+        self.port = port
+
+    def request(self, method, url, body=None, headers=None):
+        # Find a method we can use for this request
+        parsed = urlparse.urlparse(url)
+        scheme, netloc, path, params, query, fragment = parsed
+        qs = parse_qs(query)
+        if path.endswith('/'):
+            path = path[:-1]
+        meth_name = path.replace('/','_').replace('.', '_').replace('-','_')
+        if self.type:
+            meth_name = '%s_%s' % (meth_name, self.type) 
+        if self.use_param:
+            param = qs[self.use_param][0].replace('.', '_')
+            meth_name = '%s_%s' % (meth_name, param)
+        meth = getattr(self, meth_name)
+        status, body, headers, reason = meth(method, url, body, headers)
+        self.response = self.responseCls(status, body, headers, reason)
+
+    def getresponse(self):
+        return self.response
+
+    def connect(self):
+        """
+        Can't think of anything to mock here.
+        """
+        pass
+
+    def close(self):
+        pass
+
+    # Mock request/response example
+    def _example(self, method, url, body, headers):
+        """
+        Return a simple message and header, regardless of input.
+        """
+        return (httplib.OK, 'Hello World!', {'X-Foo': 'libcloud'},
+                httplib.responses[httplib.OK])
+
+    def _example_fail(self, method, url, body, headers):
+        return (httplib.FORBIDDEN, 'Oh Noes!', {'X-Foo': 'fail'},
+                httplib.responses[httplib.FORBIDDEN])
+
+
+class TestCaseMixin(object):
+
+    def test_list_nodes_response(self):
+        nodes = self.driver.list_nodes()
+        self.assertTrue(isinstance(nodes, list))
+        for node in nodes:
+            self.assertTrue(isinstance(node, Node))
+
+    def test_list_sizes_response(self):
+        sizes = self.driver.list_sizes()
+        size = sizes[0]
+        self.assertTrue(isinstance(sizes, list))
+        # Check that size values are ints or None
+        self.assertTrue(size.ram is None or isinstance(size.ram, int))
+        self.assertTrue(size.disk is None or isinstance(size.disk, int))
+        self.assertTrue(size.bandwidth is None or
+                            isinstance(size.bandwidth, int))
+
+    def test_list_images_response(self):
+        images = self.driver.list_images()
+        self.assertTrue(isinstance(images, list))
+        for image in images:
+            self.assertTrue(isinstance(image, NodeImage))
+
+
+    def test_list_images_response(self):
+        locations = self.driver.list_locations()
+        self.assertTrue(isinstance(locations, list))
+        for dc in locations:
+            self.assertTrue(isinstance(dc, NodeLocation))
+
+    def test_create_node_response(self):
+        # should return a node object
+        size = self.driver.list_sizes()[0]
+        image = self.driver.list_images()[0]
+        node = self.driver.create_node(name='node-name',
+                                     image=image,
+                                     size=size)
+        self.assertTrue(isinstance(node, Node))
+
+    def test_destroy_node_response(self):
+        # should return a node object
+        node = self.driver.list_nodes()[0]
+        ret = self.driver.destroy_node(node)
+        self.assertTrue(isinstance(ret, bool))
+
+    def test_reboot_node_response(self):
+        # should return a node object
+        node = self.driver.list_nodes()[0]
+        ret = self.driver.reboot_node(node)
+        self.assertTrue(isinstance(ret, bool))
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
+
diff --git a/test/disabled_test_dummy.py b/test/disabled_test_dummy.py
new file mode 100644
index 0000000..3e9b150
--- /dev/null
+++ b/test/disabled_test_dummy.py
@@ -0,0 +1,34 @@
+# 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.
+# libcloud.org 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 unittest
+
+from libcloud.providers import connect
+from libcloud.types import Provider, Node
+
+class DummyTests(unittest.TestCase):
+
+    def setUp(self):
+        self.conn = connect(Provider.DUMMY, 'foo')
+
+    def test_list_nodes(self):
+        ret = self.conn.list_nodes()
+
+    def test_reboot_node(self):
+        node = Node(None, None, None, None, None, attrs={})
+        ret = self.conn.reboot_node(node)
+
+    def test_create_node(self):
+        node = Node(None, None, None, None, None, attrs={})
+        ret = self.conn.create_node(node)
diff --git a/test/disabled_test_gogrid.py b/test/disabled_test_gogrid.py
new file mode 100644
index 0000000..96356d6
--- /dev/null
+++ b/test/disabled_test_gogrid.py
@@ -0,0 +1,28 @@
+# 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.
+# libcloud.org 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 unittest
+
+from libcloud.providers import connect
+from libcloud.types import Provider
+
+from secrets import GOGRID_API_KEY, GOGRID_SECRET 
+
+class GoGridTests(unittest.TestCase):
+
+    def setUp(self):
+        self.conn = connect(Provider.GOGRID, GOGRID_API_KEY, GOGRID_SECRET)
+
+    def test_list_nodes(self):
+        ret = self.conn.list_nodes()
diff --git a/test/disabled_test_vpsnet.py b/test/disabled_test_vpsnet.py
new file mode 100644
index 0000000..8a4aaf7
--- /dev/null
+++ b/test/disabled_test_vpsnet.py
@@ -0,0 +1,28 @@
+# 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.
+# libcloud.org 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 unittest
+
+from libcloud.providers import connect
+from libcloud.types import Provider
+
+from secrets import VPSNET_USER, VPSNET_KEY
+
+class VPSNetTests(unittest.TestCase):
+
+    def setUp(self):
+        self.conn = connect(Provider.VPSNET, VPSNET_USER, VPSNET_KEY)
+
+    def test_list_nodes(self):
+        ret = self.conn.list_nodes()
diff --git a/test/secrets.py-dist b/test/secrets.py-dist
new file mode 100644
index 0000000..8f61adb
--- /dev/null
+++ b/test/secrets.py-dist
@@ -0,0 +1,43 @@
+# Licensed to libcloud.org under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# libcloud.org 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.
+
+# Make a copy of this file named 'secrets.py' and add your credentials there.
+# Note you can run unit tests without setting your credentials.
+
+# for test_ec2.py
+EC2_ACCESS_ID='YoUR K3Y'
+EC2_SECRET='secr3t'
+
+RACKSPACE_USER=''
+RACKSPACE_KEY=''
+
+SLICEHOST_KEY=''
+
+VPSNET_USER=''
+VPSNET_KEY=''
+
+GOGRID_API_KEY=''
+GOGRID_SECRET=''
+
+LINODE_KEY=''
+
+HOSTINGCOM_USER=''
+HOSTINGCOM_SECRET=''
+
+TERREMARK_USER=''
+TERREMARK_SECRET=''
+
+VOXEL_KEY=''
+VOXEL_SECRET=''
diff --git a/test/test_base.py b/test/test_base.py
new file mode 100644
index 0000000..d1cd63f
--- /dev/null
+++ b/test/test_base.py
@@ -0,0 +1,93 @@
+# 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.
+# libcloud.org 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 unittest
+
+from libcloud.providers import DRIVERS, get_driver
+from libcloud.types import InvalidCredsException, Provider
+from libcloud.interface import INodeDriver
+from zope.interface.verify import verifyObject
+from zope.interface.exceptions import BrokenImplementation
+
+from libcloud.interface import IResponse, INode, INodeSize, INodeImage, INodeDriver
+from libcloud.interface import IConnectionKey, IConnectionUserAndKey
+from libcloud.base import Response, Node, NodeSize, NodeImage, NodeDriver
+from libcloud.base import ConnectionKey, ConnectionUserAndKey
+
+from test import MockResponse
+
+class FakeDriver(object):
+    type = 0 
+
+class BaseTests(unittest.TestCase):
+
+    def test_base_node(self):
+        node = Node(id=0, name=0, state=0, public_ip=0, private_ip=0, 
+            driver=FakeDriver())
+        verifyObject(INode, node)
+
+    def test_base_node_size(self):
+        node_size = NodeSize(id=0, name=0, ram=0, disk=0, bandwidth=0, price=0,
+            driver=FakeDriver())
+        verifyObject(INodeSize, node_size)
+
+    def test_base_node_image(self):
+        node_image = NodeImage(id=0, name=0, driver=FakeDriver())
+        verifyObject(INodeImage, node_image)
+
+    def test_base_response(self):
+        verifyObject(IResponse, Response(MockResponse(status=200,
+                                                      body='foo')))
+
+    def test_base_node_driver(self):
+        node_driver = NodeDriver('foo')
+        verifyObject(INodeDriver, node_driver)
+
+    def test_base_connection_key(self):
+        conn = ConnectionKey('foo')
+        verifyObject(IConnectionKey, conn)
+        
+    def test_base_connection_userkey(self):
+        conn = ConnectionUserAndKey('foo', 'bar')
+        verifyObject(IConnectionUserAndKey, conn)
+
+#    def test_drivers_interface(self):
+#        failures = []
+#        for driver in DRIVERS:
+#            creds = ProviderCreds(driver, 'foo', 'bar')
+#            try:
+#                verifyObject(INodeDriver, get_driver(driver)(creds))
+#            except BrokenImplementation:
+#                failures.append(DRIVERS[driver][1])
+#
+#        if failures:
+#            self.fail('the following drivers do not support the \
+#                       INodeDriver interface: %s' % (', '.join(failures)))
+
+#    def test_invalid_creds(self):
+#        failures = []
+#        for driver in DRIVERS:
+#            if driver == Provider.DUMMY:
+#                continue
+#            conn = connect(driver, 'bad', 'keys')
+#            try:
+#                conn.list_nodes()
+#            except InvalidCredsException:
+#                pass
+#            else:
+#                failures.append(DRIVERS[driver][1])
+#
+#        if failures:
+#            self.fail('the following drivers did not throw an \
+#                       InvalidCredsException: %s' % (', '.join(failures)))
diff --git a/test/test_ec2.py b/test/test_ec2.py
new file mode 100644
index 0000000..e09bf91
--- /dev/null
+++ b/test/test_ec2.py
@@ -0,0 +1,187 @@
+# 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.
+# libcloud.org 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 unittest
+
+from libcloud.drivers.ec2 import EC2NodeDriver
+from libcloud.base import Node, NodeImage, NodeSize
+
+from test import MockHttp, TestCaseMixin
+
+import httplib
+
+from secrets import EC2_ACCESS_ID, EC2_SECRET
+
+class EC2Tests(unittest.TestCase, TestCaseMixin):
+
+    def setUp(self):
+        EC2NodeDriver.connectionCls.conn_classes = (None, EC2MockHttp)
+        EC2MockHttp.use_param = 'Action'
+        self.driver = EC2NodeDriver(EC2_ACCESS_ID, EC2_SECRET)
+
+    def test_create_node(self):
+        image = NodeImage(id='ami-be3adfd7', 
+                          name='ec2-public-images/fedora-8-i386-base-v1.04.manifest.xml',
+                          driver=self.driver)
+        size = NodeSize('m1.small', 'Small Instance', None, None, None, None, driver=self.driver)
+        node = self.driver.create_node(name='foo', image=image, size=size)
+        self.assertEqual(node.id, 'i-2ba64342')
+
+    def test_list_nodes(self):
+        node = self.driver.list_nodes()[0]
+        self.assertEqual(node.id, 'i-4382922a')
+
+    def test_reboot_node(self):
+        node = Node('i-4382922a', None, None, None, None, self.driver)
+        ret = self.driver.reboot_node(node)
+        self.assertTrue(ret)
+
+    def test_destroy_node(self):
+        node = Node('i-4382922a', None, None, None, None, self.driver)
+        ret = self.driver.destroy_node(node)
+        self.assertTrue(ret)
+
+    def test_list_sizes(self):
+        sizes = self.driver.list_sizes()
+        self.assertEqual(len(sizes), 7)
+        self.assertTrue('m1.small' in [ s.id for s in sizes])
+        self.assertTrue('m1.large' in [ s.id for s in sizes])
+        self.assertTrue('m1.xlarge' in [ s.id for s in sizes])
+        self.assertTrue('m2.2xlarge' in [ s.id for s in sizes])
+
+    def test_list_images(self):
+        images = self.driver.list_images()
+        image = images[0]
+        self.assertEqual(len(images), 1)
+        self.assertEqual(image.name, 'ec2-public-images/fedora-8-i386-base-v1.04.manifest.xml')
+        self.assertEqual(image.id, 'ami-be3adfd7')
+
+class EC2MockHttp(MockHttp):
+    def _DescribeInstances(self, method, url, body, headers):
+        body = """<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+    <requestId>56d0fffa-8819-4658-bdd7-548f143a86d2</requestId>
+    <reservationSet>
+        <item>
+            <reservationId>r-07adf66e</reservationId>
+            <ownerId>822272953071</ownerId>
+            <groupSet>
+                <item>
+                    <groupId>default</groupId>
+                </item>
+            </groupSet>
+            <instancesSet>
+                <item>
+                    <instanceId>i-4382922a</instanceId>
+                    <imageId>ami-0d57b264</imageId>
+                    <instanceState>
+                        <code>0</code>
+                        <name>pending</name>
+                    </instanceState>
+                    <privateDnsName/>
+                    <dnsName/>
+                    <reason/>
+                    <amiLaunchIndex>0</amiLaunchIndex>
+                    <productCodes/>
+                    <instanceType>m1.small</instanceType>
+                    <launchTime>2009-08-07T05:47:04.000Z</launchTime>
+                    <placement>
+                        <availabilityZone>us-east-1a</availabilityZone>
+                    </placement>
+                    <monitoring>
+                        <state>disabled</state>
+                    </monitoring>
+                </item>
+            </instancesSet>
+        </item>
+    </reservationSet>
+</DescribeInstancesResponse>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _RebootInstances(self, method, url, body, headers):
+        body = """<RebootInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+    <requestId>76dabb7a-fb39-4ed1-b5e0-31a4a0fdf5c0</requestId>
+    <return>true</return>
+</RebootInstancesResponse>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _DescribeImages(self, method, url, body, headers):
+        body = """<DescribeImagesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+                  <imagesSet>
+                    <item>
+                      <imageId>ami-be3adfd7</imageId>
+                      <imageLocation>ec2-public-images/fedora-8-i386-base-v1.04.manifest.xml</imageLocation>
+                      <imageState>available</imageState>
+                      <imageOwnerId>206029621532</imageOwnerId>
+                      <isPublic>false</isPublic>
+                      <architecture>i386</architecture>
+                      <imageType>machine</imageType>
+                      <kernelId>aki-4438dd2d</kernelId>
+                      <ramdiskId>ari-4538dd2c</ramdiskId>
+                    </item>
+                  </imagesSet>
+                </DescribeImagesResponse>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _RunInstances(self, method, url, body, headers):
+        body = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+                      <reservationId>r-47a5402e</reservationId>
+                      <ownerId>AIDADH4IGTRXXKCD</ownerId>
+                      <groupSet>
+                        <item>
+                          <groupId>default</groupId>
+                        </item>
+                      </groupSet>
+                      <instancesSet>
+                        <item>
+                          <instanceId>i-2ba64342</instanceId>
+                          <imageId>ami-be3adfd7</imageId>
+                          <instanceState>
+                            <code>0</code>
+                            <name>pending</name>
+                          </instanceState>
+                          <privateDnsName></privateDnsName>
+                          <dnsName></dnsName>
+                          <keyName>example-key-name</keyName>
+                          <amiLaunchIndex>0</amiLaunchIndex>
+                          <instanceType>m1.small</instanceType>
+                          <launchTime>2007-08-07T11:51:50.000Z</launchTime>
+                          <placement>
+                            <availabilityZone>us-east-1b</availabilityZone>
+                          </placement>
+                          <monitoring>
+                            <enabled>true</enabled>
+                          </monitoring>
+                        </item>
+                      </instancesSet>
+                    </RunInstancesResponse>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _TerminateInstances(self, method, url, body, headers):
+        body = """<TerminateInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2009-04-04/">
+    <requestId>fa63083d-e0f7-4933-b31a-f266643bdee8</requestId>
+    <instancesSet>
+        <item>
+            <instanceId>i-4382922a</instanceId>
+            <shutdownState>
+                <code>32</code>
+                <name>shutting-down</name>
+            </shutdownState>
+            <previousState>
+                <code>16</code>
+                <name>running</name>
+            </previousState>
+        </item>
+    </instancesSet>
+</TerminateInstancesResponse>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
diff --git a/test/test_linode.py b/test/test_linode.py
new file mode 100644
index 0000000..2ee76cd
--- /dev/null
+++ b/test/test_linode.py
@@ -0,0 +1,133 @@
+# 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.
+# libcloud.org 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.
+#
+# Maintainer: Jed Smith <jsmith@linode.com>
+# Based upon code written by Alex Polvi <polvi@cloudkick.com>
+#
+
+from libcloud.drivers.linode import LinodeNodeDriver
+from libcloud.base import Node, NodeAuthPassword
+from test import MockHttp, TestCaseMixin
+
+import unittest
+import httplib
+
+class LinodeTest(unittest.TestCase, TestCaseMixin):
+    # The Linode test suite
+    
+    def setUp(self):
+        LinodeNodeDriver.connectionCls.conn_classes = (None, LinodeMockHttp)
+        LinodeMockHttp.use_param = 'api_action'
+        self.driver = LinodeNodeDriver('foo')
+
+    def test_list_nodes(self):
+        nodes = self.driver.list_nodes()
+        self.assertEqual(len(nodes), 1)
+        node = nodes[0]
+        self.assertEqual(node.id, 8098)
+        self.assertEqual(node.name, 'api-node3')
+        self.assertTrue('75.127.96.245' in node.public_ip)
+        self.assertEqual(node.private_ip, [])
+    
+    def test_reboot_node(self):
+        # An exception would indicate failure
+        node = self.driver.list_nodes()[0]
+        self.driver.reboot_node(node)
+
+    def test_destroy_node(self):
+        # An exception would indicate failure
+        node = self.driver.list_nodes()[0]
+        self.driver.destroy_node(node)
+    
+    def test_create_node(self):
+        # Will exception on failure
+        node = self.driver.create_node(name="Test",
+                         location=self.driver.list_locations()[0],
+                         size=self.driver.list_sizes()[0],
+                         image=self.driver.list_images()[6],
+                         auth=NodeAuthPassword("test123"))
+    
+    def test_list_sizes(self):
+        sizes = self.driver.list_sizes()
+        self.assertEqual(len(sizes), 10)
+        for size in sizes:
+            self.assertEqual(size.ram, int(size.name.split(" ")[1]))
+    
+    def test_list_images(self):
+        images = self.driver.list_images()
+        self.assertEqual(len(images), 22)
+        
+    def test_create_node_response(self):
+        # should return a node object
+        node = self.driver.create_node(name="node-name",
+                         location=self.driver.list_locations()[0],
+                         size=self.driver.list_sizes()[0],
+                         image=self.driver.list_images()[0],
+                         auth=NodeAuthPassword("foobar"))
+        self.assertTrue(isinstance(node, Node))
+
+        
+class LinodeMockHttp(MockHttp):
+    def _avail_datacenters(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"avail.datacenters","DATA":[{"DATACENTERID":2,"LOCATION":"Dallas, TX, USA"},{"DATACENTERID":3,"LOCATION":"Fremont, CA, USA"},{"DATACENTERID":4,"LOCATION":"Atlanta, GA, USA"},{"DATACENTERID":6,"LOCATION":"Newark, NJ, USA"}]}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _avail_linodeplans(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"avail.linodeplans","DATA":[{"AVAIL":{"2":27,"3":0,"4":0,"6":0},"DISK":16,"PRICE":19.95,"PLANID":1,"LABEL":"Linode 360","RAM":360,"XFER":200},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":24,"PRICE":29.95,"PLANID":2,"LABEL":"Linode 540","RAM":540,"XFER":300},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":32,"PRICE":39.95,"PLANID":3,"LABEL":"Linode 720","RAM":720,"XFER":400},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":48,"PRICE":59.95,"PLANID":4,"LABEL":"Linode 1080","RAM":1080,"XFER":600},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":64,"PRICE":79.95,"PLANID":5,"LABEL":"Linode 1440","RAM":1440,"XFER":800},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":128,"PRICE":159.95,"PLANID":6,"LABEL":"Linode 2880","RAM":2880,"XFER":1600},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":256,"PRICE":319.95,"PLANID":7,"LABEL":"Linode 5760","RAM":5760,"XFER":2000},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":384,"PRICE":479.95,"PLANID":8,"LABEL":"Linode 8640","RAM":8640,"XFER":2000},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":512,"PRICE":639.95,"PLANID":9,"LABEL":"Linode 11520","RAM":11520,"XFER":2000},{"AVAIL":{"2":0,"3":0,"4":0,"6":0},"DISK":640,"PRICE":799.95,"PLANID":10,"LABEL":"Linode 14400","RAM":14400,"XFER":2000}]}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _avail_distributions(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"avail.distributions","DATA":[{"IS64BIT":0,"LABEL":"Arch Linux 2007.08","MINIMAGESIZE":436,"DISTRIBUTIONID":38,"CREATE_DT":"2007-10-24 00:00:00.0"},{"IS64BIT":0,"LABEL":"Centos 5.0","MINIMAGESIZE":594,"DISTRIBUTIONID":32,"CREATE_DT":"2007-04-27 00:00:00.0"},{"IS64BIT":0,"LABEL":"Centos 5.2","MINIMAGESIZE":950,"DISTRIBUTIONID":46,"CREATE_DT":"2008-11-30 00:00:00.0"},{"IS64BIT":1,"LABEL":"Centos 5.2 64bit","MINIMAGESIZE":980,"DISTRIBUTIONID":47,"CREATE_DT":"2008-11-30 00:00:00.0"},{"IS64BIT":0,"LABEL":"Debian 4.0","MINIMAGESIZE":200,"DISTRIBUTIONID":28,"CREATE_DT":"2007-04-18 00:00:00.0"},{"IS64BIT":1,"LABEL":"Debian 4.0 64bit","MINIMAGESIZE":220,"DISTRIBUTIONID":48,"CREATE_DT":"2008-12-02 00:00:00.0"},{"IS64BIT":0,"LABEL":"Debian 5.0","MINIMAGESIZE":200,"DISTRIBUTIONID":50,"CREATE_DT":"2009-02-19 00:00:00.0"},{"IS64BIT":1,"LABEL":"Debian 5.0 64bit","MINIMAGESIZE":300,"DISTRIBUTIONID":51,"CREATE_DT":"2009-02-19 00:00:00.0"},{"IS64BIT":0,"LABEL":"Fedora 8","MINIMAGESIZE":740,"DISTRIBUTIONID":40,"CREATE_DT":"2007-11-09 00:00:00.0"},{"IS64BIT":0,"LABEL":"Fedora 9","MINIMAGESIZE":1175,"DISTRIBUTIONID":43,"CREATE_DT":"2008-06-09 15:15:21.0"},{"IS64BIT":0,"LABEL":"Gentoo 2007.0","MINIMAGESIZE":1800,"DISTRIBUTIONID":35,"CREATE_DT":"2007-08-29 00:00:00.0"},{"IS64BIT":0,"LABEL":"Gentoo 2008.0","MINIMAGESIZE":1500,"DISTRIBUTIONID":52,"CREATE_DT":"2009-03-20 00:00:00.0"},{"IS64BIT":1,"LABEL":"Gentoo 2008.0 64bit","MINIMAGESIZE":2500,"DISTRIBUTIONID":53,"CREATE_DT":"2009-04-04 00:00:00.0"},{"IS64BIT":0,"LABEL":"OpenSUSE 11.0","MINIMAGESIZE":850,"DISTRIBUTIONID":44,"CREATE_DT":"2008-08-21 08:32:16.0"},{"IS64BIT":0,"LABEL":"Slackware 12.0","MINIMAGESIZE":315,"DISTRIBUTIONID":34,"CREATE_DT":"2007-07-16 00:00:00.0"},{"IS64BIT":0,"LABEL":"Slackware 12.2","MINIMAGESIZE":500,"DISTRIBUTIONID":54,"CREATE_DT":"2009-04-04 00:00:00.0"},{"IS64BIT":0,"LABEL":"Ubuntu 8.04 LTS","MINIMAGESIZE":400,"DISTRIBUTIONID":41,"CREATE_DT":"2008-04-23 15:11:29.0"},{"IS64BIT":1,"LABEL":"Ubuntu 8.04 LTS 64bit","MINIMAGESIZE":350,"DISTRIBUTIONID":42,"CREATE_DT":"2008-06-03 12:51:11.0"},{"IS64BIT":0,"LABEL":"Ubuntu 8.10","MINIMAGESIZE":220,"DISTRIBUTIONID":45,"CREATE_DT":"2008-10-30 23:23:03.0"},{"IS64BIT":1,"LABEL":"Ubuntu 8.10 64bit","MINIMAGESIZE":230,"DISTRIBUTIONID":49,"CREATE_DT":"2008-12-02 00:00:00.0"},{"IS64BIT":0,"LABEL":"Ubuntu 9.04","MINIMAGESIZE":350,"DISTRIBUTIONID":55,"CREATE_DT":"2009-04-23 00:00:00.0"},{"IS64BIT":1,"LABEL":"Ubuntu 9.04 64bit","MINIMAGESIZE":350,"DISTRIBUTIONID":56,"CREATE_DT":"2009-04-23 00:00:00.0"}]}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _linode_create(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"linode.create","DATA":{"LinodeID":8098}}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _linode_disk_createfromdistribution(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"linode.disk.createFromDistribution","DATA":{"JobID":1298,"DiskID":55647}}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _linode_delete(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"linode.delete","DATA":{"LinodeID":8098}}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _linode_reboot(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"linode.reboot","DATA":{"JobID":1305}}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _avail_kernels(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"avail.kernels","DATA":[{"LABEL":"Latest 2.6 Stable (2.6.18.8-linode19)","ISXEN":1,"KERNELID":60},{"LABEL":"2.6.18.8-linode19","ISXEN":1,"KERNELID":103},{"LABEL":"2.6.30.5-linode20","ISXEN":1,"KERNELID":105},{"LABEL":"Latest 2.6 Stable (2.6.18.8-x86_64-linode7)","ISXEN":1,"KERNELID":107},{"LABEL":"2.6.18.8-x86_64-linode7","ISXEN":1,"KERNELID":104},{"LABEL":"2.6.30.5-x86_64-linode8","ISXEN":1,"KERNELID":106},{"LABEL":"pv-grub-x86_32","ISXEN":1,"KERNELID":92},{"LABEL":"pv-grub-x86_64","ISXEN":1,"KERNELID":95},{"LABEL":"Recovery - Finnix (kernel)","ISXEN":1,"KERNELID":61},{"LABEL":"2.6.18.8-domU-linode7","ISXEN":1,"KERNELID":81},{"LABEL":"2.6.18.8-linode10","ISXEN":1,"KERNELID":89},{"LABEL":"2.6.18.8-linode16","ISXEN":1,"KERNELID":98},{"LABEL":"2.6.24.4-linode8","ISXEN":1,"KERNELID":84},{"LABEL":"2.6.25-linode9","ISXEN":1,"KERNELID":88},{"LABEL":"2.6.25.10-linode12","ISXEN":1,"KERNELID":90},{"LABEL":"2.6.26-linode13","ISXEN":1,"KERNELID":91},{"LABEL":"2.6.27.4-linode14","ISXEN":1,"KERNELID":93},{"LABEL":"2.6.28-linode15","ISXEN":1,"KERNELID":96},{"LABEL":"2.6.28.3-linode17","ISXEN":1,"KERNELID":99},{"LABEL":"2.6.29-linode18","ISXEN":1,"KERNELID":101},{"LABEL":"2.6.16.38-x86_64-linode2","ISXEN":1,"KERNELID":85},{"LABEL":"2.6.18.8-x86_64-linode1","ISXEN":1,"KERNELID":86},{"LABEL":"2.6.27.4-x86_64-linode3","ISXEN":1,"KERNELID":94},{"LABEL":"2.6.28-x86_64-linode4","ISXEN":1,"KERNELID":97},{"LABEL":"2.6.28.3-x86_64-linode5","ISXEN":1,"KERNELID":100},{"LABEL":"2.6.29-x86_64-linode6","ISXEN":1,"KERNELID":102}]}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _linode_disk_create(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"linode.disk.create","DATA":{"JobID":1299,"DiskID":55648}}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _linode_boot(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"linode.boot","DATA":{"JobID":1300}}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    
+    def _linode_config_create(self, method, url, body, headers):
+        body = '{"ERRORARRAY":[],"ACTION":"linode.config.create","DATA":{"ConfigID":31239}}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _linode_list(self, method, url, body, headers):
+        body = '{"ACTION": "linode.list", "DATA": [{"ALERT_DISKIO_ENABLED": 1, "BACKUPWEEKLYDAY": 0, "LABEL": "api-node3", "DATACENTERID": 5, "ALERT_BWOUT_ENABLED": 1, "ALERT_CPU_THRESHOLD": 10, "TOTALHD": 100, "ALERT_BWQUOTA_THRESHOLD": 81, "ALERT_BWQUOTA_ENABLED": 1, "TOTALXFER": 200, "STATUS": 2, "ALERT_BWIN_ENABLED": 1, "ALERT_BWIN_THRESHOLD": 5, "ALERT_DISKIO_THRESHOLD": 200, "WATCHDOG": 1, "LINODEID": 8098, "BACKUPWINDOW": 1, "TOTALRAM": 540, "LPM_DISPLAYGROUP": "", "ALERT_BWOUT_THRESHOLD": 5, "BACKUPSENABLED": 1, "ALERT_CPU_ENABLED": 1}], "ERRORARRAY": []}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _linode_ip_list(self, method, url, body, headers):
+        body = '{"ACTION": "linode.ip.list", "DATA": [{"RDNS_NAME": "li22-54.members.linode.com", "ISPUBLIC": 1, "IPADDRESS": "75.127.96.54", "IPADDRESSID": 5384, "LINODEID": 8098}, {"RDNS_NAME": "li22-245.members.linode.com", "ISPUBLIC": 1, "IPADDRESS": "75.127.96.245", "IPADDRESSID": 5575, "LINODEID": 8098}], "ERRORARRAY": []}'
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
diff --git a/test/test_rackspace.py b/test/test_rackspace.py
new file mode 100644
index 0000000..9fca8a1
--- /dev/null
+++ b/test/test_rackspace.py
@@ -0,0 +1,144 @@
+# 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.
+# libcloud.org 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 unittest
+
+from libcloud.types import InvalidCredsException
+from libcloud.drivers.rackspace import RackspaceNodeDriver as Rackspace
+from libcloud.base import Node, NodeImage, NodeSize
+
+from test import MockHttp, TestCaseMixin
+from secrets import RACKSPACE_USER, RACKSPACE_KEY
+import httplib
+
+class RackspaceTests(unittest.TestCase, TestCaseMixin):
+
+    def setUp(self):
+        Rackspace.connectionCls.conn_classes = (None, RackspaceMockHttp)
+        RackspaceMockHttp.type = None
+        self.driver = Rackspace(RACKSPACE_USER, RACKSPACE_KEY)
+
+    def test_auth(self):
+        RackspaceMockHttp.type = 'UNAUTHORIZED'
+        try:
+            self.driver = Rackspace(RACKSPACE_USER, RACKSPACE_KEY)
+        except InvalidCredsException, e:
+            self.assertEqual(True, isinstance(e, InvalidCredsException))
+        else:
+            self.fail('test should have thrown')
+
+    def test_list_nodes(self):
+        RackspaceMockHttp.type = 'EMPTY'
+        ret = self.driver.list_nodes()
+        self.assertEqual(len(ret), 0)
+        RackspaceMockHttp.type = None
+        ret = self.driver.list_nodes()
+        self.assertEqual(len(ret), 1)
+        node = ret[0]
+        self.assertEqual('67.23.21.33', node.public_ip[0])
+        self.assertEqual('10.176.168.218', node.private_ip[0])
+        self.assertEqual(node.extra.get('flavorId'), '1')
+        self.assertEqual(node.extra.get('imageId'), '11')
+
+    def test_list_sizes(self):
+        ret = self.driver.list_sizes()
+        self.assertEqual(len(ret), 7)
+        size = ret[0]
+        self.assertEqual(size.name, '256 slice')
+
+    def test_list_images(self):
+        ret = self.driver.list_images()
+        self.assertEqual(ret[10].extra['serverId'], None)
+        self.assertEqual(ret[11].extra['serverId'], '91221')
+
+    def test_create_node(self):
+        image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)', driver=self.driver)
+        size = NodeSize(1, '256 slice', None, None, None, None, driver=self.driver)
+        node = self.driver.create_node(name='racktest', image=image, size=size)
+        self.assertEqual(node.name, 'racktest')
+        self.assertEqual(node.extra.get('password'), 'racktestvJq7d3')
+
+    def test_create_node_with_metadata(self):
+        RackspaceMockHttp.type = 'METADATA'
+        image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)', driver=self.driver)
+        size = NodeSize(1, '256 slice', None, None, None, None, driver=self.driver)
+        metadata = { 'a': 'b', 'c': 'd' }
+        files = { '/file1': 'content1', '/file2': 'content2' }
+        node = self.driver.create_node(name='racktest', image=image, size=size, metadata=metadata, files=files)
+        self.assertEqual(node.name, 'racktest')
+        self.assertEqual(node.extra.get('password'), 'racktestvJq7d3')
+        self.assertEqual(node.extra.get('metadata'), metadata)
+
+    def test_reboot_node(self):
+        node = Node(id=72258, name=None, state=None, public_ip=None, private_ip=None,
+                    driver=self.driver)
+        ret = node.reboot()
+        self.assertTrue(ret is True)
+
+    def test_destroy_node(self):
+        node = Node(id=72258, name=None, state=None, public_ip=None, private_ip=None,
+                    driver=self.driver)
+        ret = node.destroy()
+        self.assertTrue(ret is True)
+
+class RackspaceMockHttp(MockHttp):
+
+    # fake auth token response
+    def _v1_0(self, method, url, body, headers):
+        headers = {'x-server-management-url': 'https://servers.api.rackspacecloud.com/v1.0/slug',
+                   'x-auth-token': 'FE011C19-CF86-4F87-BE5D-9229145D7A06',
+                   'x-cdn-management-url': 'https://cdn.clouddrive.com/v1/MossoCloudFS_FE011C19-CF86-4F87-BE5D-9229145D7A06',
+                   'x-storage-token': 'FE011C19-CF86-4F87-BE5D-9229145D7A06',
+                   'x-storage-url': 'https://storage4.clouddrive.com/v1/MossoCloudFS_FE011C19-CF86-4F87-BE5D-9229145D7A06'}
+
+        return (httplib.NO_CONTENT, "", headers, httplib.responses[httplib.NO_CONTENT])
+    def _v1_0_UNAUTHORIZED(self, method, url, body, headers):
+        return  (httplib.UNAUTHORIZED, "", {}, httplib.responses[httplib.UNAUTHORIZED])
+
+    def _v1_0_slug_servers_detail_EMPTY(self, method, url, body, headers):
+        body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><servers xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"/>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _v1_0_slug_servers_detail(self, method, url, body, headers):
+        body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><servers xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"><server status="ACTIVE" progress="100" hostId="9dd380940fcbe39cb30255ed4664f1f3" flavorId="1" imageId="11" id="72258" name="racktest"><metadata/><addresses><public><ip addr="67.23.21.33"/></public><private><ip addr="10.176.168.218"/></private></addresses></server></servers>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _v1_0_slug_flavors_detail(self, method, url, body, headers):
+        body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><flavors xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"><flavor disk="10" ram="256" name="256 slice" id="1"/><flavor disk="20" ram="512" name="512 slice" id="2"/><flavor disk="40" ram="1024" name="1GB slice" id="3"/><flavor disk="80" ram="2048" name="2GB slice" id="4"/><flavor disk="160" ram="4096" name="4GB slice" id="5"/><flavor disk="320" ram="8192" name="8GB slice" id="6"/><flavor disk="620" ram="15872" name="15.5GB slice" id="7"/></flavors>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _v1_0_slug_images_detail(self, method, url, body, headers):
+        body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><images xmlns="http://docs.rackspacecloud.com/servers/api/v1.0"><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="CentOS 5.2" id="2"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Gentoo 2008.0" id="3"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Debian 5.0 (lenny)" id="4"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Fedora 10 (Cambridge)" id="5"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="CentOS 5.3" id="7"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Ubuntu 9.04 (jaunty)" id="8"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Arch 2009.02" id="9"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Ubuntu 8.04.2 LTS (hardy)" id="10"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Ubuntu 8.10 (intrepid)" id="11"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Red Hat EL 5.3" id="12"/><image status="ACTIVE" created="2009-07-20T09:14:37-05:00" updated="2009-07-20T09:14:37-05:00" name="Fedora 11 (Leonidas)" id="13"/><image status="ACTIVE" progress="100" created="2009-11-29T20:22:09-06:00" updated="2009-11-29T20:24:08-06:00" serverId="91221" name="daily" id="191234"/></images>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+        
+    def _v1_0_slug_servers(self, method, url, body, headers):
+        body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" status="BUILD" progress="0" hostId="9dd380940fcbe39cb30255ed4664f1f3" flavorId="1" imageId="11" adminPass="racktestvJq7d3" id="72258" name="racktest"><metadata/><addresses><public><ip addr="67.23.21.33"/></public><private><ip addr="10.176.168.218"/></private></addresses></server>"""
+        return (httplib.ACCEPTED, body, {}, httplib.responses[httplib.ACCEPTED])
+
+    def _v1_0_slug_servers_METADATA(self, method, url, body, headers):
+        body = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?><server xmlns="http://docs.rackspacecloud.com/servers/api/v1.0" status="BUILD" progress="0" hostId="9dd380940fcbe39cb30255ed4664f1f3" flavorId="1" imageId="11" adminPass="racktestvJq7d3" id="72258" name="racktest"><metadata><meta key="a">b</meta><meta key="c">d</meta></metadata><addresses><public><ip addr="67.23.21.33"/></public><private><ip addr="10.176.168.218"/></private></addresses></server>"""
+        return (httplib.ACCEPTED, body, {}, httplib.responses[httplib.ACCEPTED])
+
+    def _v1_0_slug_servers_72258_action(self, method, url, body, headers):
+        if method != "POST" or body[:8] != "<reboot ":
+            raise NotImplemented
+        # only used by reboot() right now, but we will need to parse body someday !!!!
+        return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED])
+
+    def _v1_0_slug_servers_72258(self, method, url, body, headers):
+        if method != "DELETE":
+            raise NotImplemented
+        # only used by destroy node()
+        return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED])
+
diff --git a/test/test_rimuhosting.py b/test/test_rimuhosting.py
new file mode 100644
index 0000000..13f954a
--- /dev/null
+++ b/test/test_rimuhosting.py
@@ -0,0 +1,285 @@
+# 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.
+# libcloud.org 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.
+# Copyright 2009 RedRata Ltd
+
+from libcloud.drivers.rimuhosting import RimuHostingNodeDriver
+from test import MockHttp
+from test import MockHttp, TestCaseMixin
+
+import unittest
+import httplib
+
+class RimuHostingTest(unittest.TestCase, TestCaseMixin):
+    def setUp(self):
+        RimuHostingNodeDriver.connectionCls.conn_classes = (None,
+                                                            RimuHostingMockHttp)
+        self.driver = RimuHostingNodeDriver('foo')
+
+    def test_list_nodes(self):
+        nodes = self.driver.list_nodes()
+        self.assertEqual(len(nodes),1)
+        node = nodes[0]
+        self.assertEqual(node.public_ip[0], "1.2.3.4")
+        self.assertEqual(node.public_ip[1], "1.2.3.5")
+        self.assertEqual(node.extra['order_oid'], 88833465)
+        self.assertEqual(node.id, "order-88833465-api-ivan-net-nz")
+
+    def test_list_sizes(self):
+        sizes = self.driver.list_sizes()
+        self.assertEqual(len(sizes),1)
+        size = sizes[0]
+        self.assertEqual(size.ram,950)
+        self.assertEqual(size.disk,20)
+        self.assertEqual(size.bandwidth,75)
+        self.assertEqual(size.price,32.54)
+
+    def test_list_images(self):
+        images = self.driver.list_images()
+        self.assertEqual(len(images),6)
+        image = images[0]
+        self.assertEqual(image.name,"Debian 5.0 (aka Lenny, RimuHosting"\
+                         " recommended distro)")
+        self.assertEqual(image.id, "lenny")
+
+    def test_reboot_node(self):
+        # Raises exception on failure
+        node = self.driver.list_nodes()[0]
+        self.driver.reboot_node(node)
+
+    def test_destroy_node(self):
+        # Raises exception on failure
+        node = self.driver.list_nodes()[0]
+        self.driver.destroy_node(node)
+    
+    def test_create_node(self):
+        # Raises exception on failure
+        size = self.driver.list_sizes()[0]
+        image = self.driver.list_images()[0]
+        self.driver.create_node(name="api.ivan.net.nz", image=image, size=size)
+
+class RimuHostingMockHttp(MockHttp):
+    def _r_orders(self,method,url,body,headers):
+        body = """
+        { "get_orders_response" : 
+            { "status_message" : null
+            , "status_code" : 200 
+            , "error_info" : null 
+            , "response_type" : "OK" 
+            , "human_readable_message" : "Found 15 orders"
+            , "response_display_duration_type" : "REGULAR",
+            "about_orders" :
+                [{ "order_oid" : 88833465
+                , "domain_name" : "api.ivan.net.nz"
+                , "slug" : "order-88833465-api-ivan-net-nz"
+                , "billing_oid" : 96122465
+                , "is_on_customers_own_physical_server" : false
+                , "vps_parameters" : { "memory_mb" : 160
+                    , "disk_space_mb" : 4096
+                    , "disk_space_2_mb" : 0}
+                , "host_server_oid" : "764"
+                , "server_type" : "VPS"
+                , "data_transfer_allowance" : { "data_transfer_gb" : 30
+                    , "data_transfer" : "30"}
+                , "billing_info" : { }
+                , "allocated_ips" : { "primary_ip" : "1.2.3.4"
+                    , "secondary_ips" : ["1.2.3.5","1.2.3.6"]}
+                , "running_state" : "RUNNING"}]}}"""
+    
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    def _r_pricing_plans(self,method,url,body,headers):
+        body = """
+         {"get_pricing_plans_response" : 
+             { "status_message" : null
+              , "status_code" : 200
+              , "error_info" : null
+              , "response_type" : "OK"
+              , "human_readable_message" : "Here some pricing plans we are offering on new orders.&nbsp; Note we offer most disk and memory sizes.&nbsp; So if you setup a new server feel free to vary these (e.g. different memory, disk, etc) and we will just adjust the pricing to suit.&nbsp; Pricing is in USD.&nbsp; If you are an NZ-based customer then we would need to add GST."
+              , "response_display_duration_type" : "REGULAR"
+              , "pricing_plan_infos" : 
+                  [{ "pricing_plan_code" : "MiroVPSLowContention"
+                    , "pricing_plan_description" : "MiroVPS Semi-Dedicated Server (Dallas)"
+                    , "monthly_recurring_fee" : 32.54
+                    , "monthly_recurring_amt" : { "amt" : 35.0
+                    , "currency" : "CUR_AUD"
+                    ,"amt_usd" : 32.54}
+                    , "minimum_memory_mb" : 950
+                    , "minimum_disk_gb" : 20
+                    , "minimum_data_transfer_allowance_gb" : 75
+                    , "see_also_url" : "http://rimuhosting.com/order/serverdetails.jsp?plan=MiroVPSLowContention"
+                    , "server_type" : "VPS"
+                    , "offered_at_data_center" : 
+                        { "data_center_location_code" : "DCDALLAS"
+                        , "data_center_location_name" : "Dallas"}}
+                ]}}
+
+        """
+
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _r_distributions(self, method, url, body, headers):
+        body = """
+        { "get_distros_response" : { "status_message" : null
+  , "status_code" : 200
+  , "error_info" : null
+  , "response_type" : "OK"
+  , "human_readable_message" : "Here are the distros we are offering on new orders."
+  , "response_display_duration_type" : "REGULAR"
+  , "distro_infos" : [{ "distro_code" : "lenny"
+        , "distro_description" : "Debian 5.0 (aka Lenny, RimuHosting recommended distro)"}
+    , { "distro_code" : "centos5"
+        , "distro_description" : "Centos5"}
+    , { "distro_code" : "ubuntu904"
+        , "distro_description" : "Ubuntu 9.04 (Jaunty Jackalope, from 2009-04)"}
+    , { "distro_code" : "ubuntu804"
+        , "distro_description" : "Ubuntu 8.04 (Hardy Heron, 5 yr long term support (LTS))"}
+    , { "distro_code" : "ubuntu810"
+        , "distro_description" : "Ubuntu 8.10 (Intrepid Ibex, from 2008-10)"}
+    , { "distro_code" : "fedora10"
+        , "distro_description" : "Fedora 10"}]}}
+        """
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _r_orders_new_vps(self, method, url, body, headers):
+        body = """
+    { "post_new_vps_response" : 
+        { "status_message" : null
+          , "status_code" : 200
+          , "error_info" : null
+          , "response_type" : "OK"
+          , "human_readable_message" : null
+          , "response_display_duration_type" : "REGULAR"
+          , "setup_messages" : 
+              ["Using user-specified billing data: Wire Transfer" , "Selected user as the owner of the billing details: Ivan Meredith"
+            , "No VPS paramters provided, using default values."]
+          , "about_order" : 
+              { "order_oid" : 52255865
+              , "domain_name" : "api.ivan.net.nz"
+              , "slug" : "order-52255865-api-ivan-net-nz"
+              , "billing_oid" : 96122465
+              , "is_on_customers_own_physical_server" : false
+              , "vps_parameters" : 
+                  { "memory_mb" : 160
+                  , "disk_space_mb" : 4096
+                  , "disk_space_2_mb" : 0}
+              , "host_server_oid" : "764"
+              , "server_type" : "VPS"
+              , "data_transfer_allowance" :
+                  { "data_transfer_gb" : 30 , "data_transfer" : "30"}
+              , "billing_info" : { }
+              , "allocated_ips" : 
+                  { "primary_ip" : "74.50.57.80", "secondary_ips" : []}
+              , "running_state" : "RUNNING"}
+          , "new_order_request" : 
+              { "billing_oid" : 96122465
+              , "user_oid" : 0
+              , "host_server_oid" : null
+              , "vps_order_oid_to_clone" : 0
+              , "ip_request" : 
+                  { "num_ips" : 1, "extra_ip_reason" : ""}
+              , "vps_parameters" : 
+                  { "memory_mb" : 160
+                  , "disk_space_mb" : 4096
+                  , "disk_space_2_mb" : 0}
+              , "pricing_plan_code" : "MIRO1B"
+              , "instantiation_options" : 
+                  { "control_panel" : "webmin"
+                  , "domain_name" : "api.ivan.net.nz"
+                  , "password" : "aruxauce27"
+                  , "distro" : "lenny"}}
+          , "running_vps_info" : 
+              { "pings_ok" : true
+              , "current_kernel" : "default"
+              , "current_kernel_canonical" : "2.6.30.5-xenU.i386"
+              , "last_backup_message" : ""
+              , "is_console_login_enabled" : false
+              , "console_public_authorized_keys" : null
+              , "is_backup_running" : false
+              , "is_backups_enabled" : true
+              , "next_backup_time" : 
+                  { "ms_since_epoch": 1256446800000, "iso_format" : "2009-10-25T05:00:00Z", "users_tz_offset_ms" : 46800000}
+              , "vps_uptime_s" : 31
+              , "vps_cpu_time_s" : 6
+              , "running_state" : "RUNNING"
+              , "is_suspended" : false}}}
+
+        """
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _r_orders_order_88833465_api_ivan_net_nz_vps(self, method, url, body, headers):
+        body = """
+        { "delete_server_response" : 
+            { "status_message" : null
+            , "status_code" : 200
+            , "error_info" : null
+            , "response_type" : "OK"
+            , "human_readable_message" : "Server removed"
+            , "response_display_duration_type" : "REGULAR"
+            , "cancel_messages" : 
+                ["api.ivan.net.nz is being shut down."
+                , "A $7.98 credit has been added to your account."
+                , "If you need to un-cancel the server please contact our support team."]
+            }
+        }
+
+        """
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _r_orders_order_88833465_api_ivan_net_nz_vps_running_state(self, method,
+                                                                   url, body,
+                                                                   headers):
+        
+        body = """
+        { "put_running_state_response" : 
+            { "status_message" : null
+              , "status_code" : 200
+              , "error_info" : null
+              , "response_type" : "OK"
+              , "human_readable_message" : "api.ivan.net.nz restarted.  After the reboot api.ivan.net.nz is pinging OK."
+              , "response_display_duration_type" : "REGULAR"
+              , "is_restarted" : true
+              , "is_pinging" : true
+              , "running_vps_info" : 
+                  { "pings_ok" : true
+                  , "current_kernel" : "default"
+                  , "current_kernel_canonical" : "2.6.30.5-xenU.i386"
+                  , "last_backup_message" : ""
+                  , "is_console_login_enabled" : false
+                  , "console_public_authorized_keys" : null
+                  , "is_backup_running" : false
+                  , "is_backups_enabled" : true
+                  , "next_backup_time" : 
+                      { "ms_since_epoch": 1256446800000, "iso_format" : "2009-10-25T05:00:00Z", "users_tz_offset_ms" : 46800000}
+                  , "vps_uptime_s" : 19
+                  , "vps_cpu_time_s" : 5
+                  , "running_state" : "RUNNING"
+                  , "is_suspended" : false}
+              , "host_server_info" : { "is_host64_bit_capable" : true
+                  , "default_kernel_i386" : "2.6.30.5-xenU.i386"
+                  , "default_kernel_x86_64" : "2.6.30.5-xenU.x86_64"
+                  , "cpu_model_name" : "Intel(R) Xeon(R) CPU           E5506  @ 2.13GHz"
+                  , "host_num_cores" : 1
+                  , "host_xen_version" : "3.4.1"
+                  , "hostload" : [1.45
+                    , 0.56
+                    , 0.28]
+                  , "host_uptime_s" : 3378276
+                  , "host_mem_mb_free" : 51825
+                  , "host_mem_mb_total" : 73719
+                  , "running_vpss" : 34}
+              , "running_state_messages" : null}}
+
+        """
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
diff --git a/test/test_slicehost.py b/test/test_slicehost.py
new file mode 100644
index 0000000..20dcb95
--- /dev/null
+++ b/test/test_slicehost.py
@@ -0,0 +1,290 @@
+# 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.
+# libcloud.org 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 unittest
+
+from libcloud.drivers.slicehost import SlicehostNodeDriver as Slicehost
+from libcloud.types import Provider, NodeState
+from libcloud.base import Node, NodeImage, NodeSize
+
+import httplib
+
+from test import MockHttp, multipleresponse, TestCaseMixin
+from secrets import SLICEHOST_KEY
+from xml.etree import ElementTree as ET
+
+class SlicehostTest(unittest.TestCase, TestCaseMixin):
+
+    def setUp(self):
+
+        Slicehost.connectionCls.conn_classes = (None, SlicehostMockHttp)
+        SlicehostMockHttp.type = None
+        self.driver = Slicehost('foo')
+        #self.driver = Slicehost(SLICEHOST_KEY)
+
+    def test_list_nodes(self):
+        ret = self.driver.list_nodes()
+        self.assertEqual(len(ret), 1)
+        node = ret[0]
+        self.assertTrue('174.143.212.229' in node.public_ip)
+        self.assertTrue('10.176.164.199' in node.private_ip)
+        self.assertEqual(node.state, NodeState.PENDING)
+
+        SlicehostMockHttp.type = 'UNAUTHORIZED'
+        try:
+            ret = self.driver.list_nodes()
+        except Exception, e:
+            self.assertEqual(e.args[0], 'HTTP Basic: Access denied.')
+        else:
+            self.fail('test should have thrown')
+
+    def test_list_sizes(self):
+        ret = self.driver.list_sizes()
+        self.assertEqual(len(ret), 7)
+        size = ret[0]
+        self.assertEqual(size.name, '256 slice')
+
+    def test_list_images(self):
+        ret = self.driver.list_images()
+        self.assertEqual(len(ret), 11)
+        image = ret[0]
+        self.assertEqual(image.name, 'CentOS 5.2')
+        self.assertEqual(image.id, 2)
+
+    def test_reboot_node(self):
+        node = Node(id=1, name=None, state=None, public_ip=None, private_ip=None,
+                    driver=self.driver)
+
+        ret = node.reboot()
+        self.assertTrue(ret is True)
+
+        ret = self.driver.reboot_node(node)
+        self.assertTrue(ret is True)
+
+        SlicehostMockHttp.type = 'FORBIDDEN'
+        try:
+            ret = self.driver.reboot_node(node)
+        except Exception, e:
+            self.assertEqual(e.args[0], 'Permission denied')
+        else:
+            self.fail('test should have thrown')
+
+    def test_destroy_node(self):
+        node = Node(id=1, name=None, state=None, public_ip=None, private_ip=None,
+                    driver=self.driver)
+
+        ret = node.destroy()
+        self.assertTrue(ret is True)
+
+        ret = self.driver.destroy_node(node)
+        self.assertTrue(ret is True)
+
+    def test_create_node(self):
+        image = NodeImage(id=11, name='ubuntu 8.10', driver=self.driver)
+        size = NodeSize(1, '256 slice', None, None, None, None, driver=self.driver)
+        node = self.driver.create_node(name='slicetest', image=image, size=size)
+        self.assertEqual(node.name, 'slicetest')
+
+class SlicehostMockHttp(MockHttp):
+
+    def _slices_xml(self, method, url, body, headers):
+        if method == 'POST':
+            tree = ET.XML(body)
+            name = tree.findtext('name')
+            image_id = int(tree.findtext('image-id'))
+            flavor_id = int(tree.findtext('flavor-id'))
+
+            # TODO: would be awesome to get the slicehost api developers to fill in the
+            # the correct validation logic
+            if not (name and image_id and flavor_id) \
+                or tree.tag != 'slice' \
+                or not headers.has_key('Content-Type')  \
+                or headers['Content-Type'] != 'application/xml':
+              err_body = """<?xml version="1.0" encoding="UTF-8"?>
+<errors>
+  <error>Slice parameters are not properly nested</error>
+</errors>"""
+              return (httplib.UNPROCESSABLE_ENTITY, err_body, {}, '')
+
+            body = """<slice>
+  <name>slicetest</name>
+  <image-id type="integer">11</image-id>
+  <addresses type="array">
+    <address>10.176.168.15</address>
+    <address>67.23.20.114</address>
+  </addresses>
+  <root-password>fooadfa1231</root-password>
+  <progress type="integer">0</progress>
+  <id type="integer">71907</id>
+  <bw-out type="float">0.0</bw-out>
+  <bw-in type="float">0.0</bw-in>
+  <flavor-id type="integer">1</flavor-id>
+  <status>build</status>
+  <ip-address>10.176.168.15</ip-address>
+</slice>"""
+            return (httplib.CREATED, body, {}, '')
+        else:
+            body = """<slices type="array">
+  <slice>
+    <name>libcloud-foo</name>
+    <image-id type="integer">10</image-id>
+    <addresses type="array">
+      <address>174.143.212.229</address>
+      <address>10.176.164.199</address>
+    </addresses>
+    <progress type="integer">0</progress>
+    <id type="integer">1</id>
+    <bw-out type="float">0.0</bw-out>
+    <bw-in type="float">0.0</bw-in>
+    <flavor-id type="integer">1</flavor-id>
+    <status>build</status>
+    <ip-address>174.143.212.229</ip-address>
+  </slice>
+</slices>"""
+        
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _slices_xml_UNAUTHORIZED(self, method, url, body, headers):
+        err_body = 'HTTP Basic: Access denied.'
+        return (httplib.UNAUTHORIZED, err_body, {}, 
+                 httplib.responses[httplib.UNAUTHORIZED])
+
+    def _flavors_xml(self, method, url, body, headers):
+        body = """<?xml version="1.0" encoding="UTF-8"?>
+<flavors type="array">
+  <flavor>
+    <id type="integer">1</id>
+    <name>256 slice</name>
+    <price type="integer">2000</price>
+    <ram type="integer">256</ram>
+  </flavor>
+  <flavor>
+    <id type="integer">2</id>
+    <name>512 slice</name>
+    <price type="integer">3800</price>
+    <ram type="integer">512</ram>
+  </flavor>
+  <flavor>
+    <id type="integer">3</id>
+    <name>1GB slice</name>
+    <price type="integer">7000</price>
+    <ram type="integer">1024</ram>
+  </flavor>
+  <flavor>
+    <id type="integer">4</id>
+    <name>2GB slice</name>
+    <price type="integer">13000</price>
+    <ram type="integer">2048</ram>
+  </flavor>
+  <flavor>
+    <id type="integer">5</id>
+    <name>4GB slice</name>
+    <price type="integer">25000</price>
+    <ram type="integer">4096</ram>
+  </flavor>
+  <flavor>
+    <id type="integer">6</id>
+    <name>8GB slice</name>
+    <price type="integer">45000</price>
+    <ram type="integer">8192</ram>
+  </flavor>
+  <flavor>
+    <id type="integer">7</id>
+    <name>15.5GB slice</name>
+    <price type="integer">80000</price>
+    <ram type="integer">15872</ram>
+  </flavor>
+</flavors>
+"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _images_xml(self, method, url, body, headers):
+        body = """<?xml version="1.0" encoding="UTF-8"?>
+<images type="array">
+  <image>
+    <name>CentOS 5.2</name>
+    <id type="integer">2</id>
+  </image>
+  <image>
+    <name>Gentoo 2008.0</name>
+    <id type="integer">3</id>
+  </image>
+  <image>
+    <name>Debian 5.0 (lenny)</name>
+    <id type="integer">4</id>
+  </image>
+  <image>
+    <name>Fedora 10 (Cambridge)</name>
+    <id type="integer">5</id>
+  </image>
+  <image>
+    <name>CentOS 5.3</name>
+    <id type="integer">7</id>
+  </image>
+  <image>
+    <name>Ubuntu 9.04 (jaunty)</name>
+    <id type="integer">8</id>
+  </image>
+  <image>
+    <name>Arch 2009.02</name>
+    <id type="integer">9</id>
+  </image>
+  <image>
+    <name>Ubuntu 8.04.2 LTS (hardy)</name>
+    <id type="integer">10</id>
+  </image>
+  <image>
+    <name>Ubuntu 8.10 (intrepid)</name>
+    <id type="integer">11</id>
+  </image>
+  <image>
+    <name>Red Hat EL 5.3</name>
+    <id type="integer">12</id>
+  </image>
+  <image>
+    <name>Fedora 11 (Leonidas)</name>
+    <id type="integer">13</id>
+  </image>
+</images>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _slices_1_reboot_xml(self, method, url, body, headers):
+        body = """<slice>
+  <name>libcloud-test</name>
+  <image-id type="integer">10</image-id>
+  <addresses type="array">
+    <address>174.143.212.229</address>
+    <address>10.176.164.199</address>
+  </addresses>
+  <progress type="integer">100</progress>
+  <id type="integer">70507</id>
+  <bw-out type="float">0.0</bw-out>
+  <bw-in type="float">0.0</bw-in>
+  <flavor-id type="integer">1</flavor-id>
+  <status>reboot</status>
+  <ip-address>174.143.212.229</ip-address>
+</slice>"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+
+    def _slices_1_reboot_xml_FORBIDDEN(self, method, url, body, headers):
+        err_body = """<errors>
+  <error>Permission denied</error>
+</errors>"""
+        return (httplib.FORBIDDEN, err_body, {}, 
+                 httplib.responses[httplib.FORBIDDEN])
+
+    def _slices_1_destroy_xml(self, method, url, body, headers):
+        body = ''
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
diff --git a/test/test_vcloud.py b/test/test_vcloud.py
new file mode 100644
index 0000000..0d511b5
--- /dev/null
+++ b/test/test_vcloud.py
@@ -0,0 +1,329 @@
+# 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.
+# libcloud.org 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 unittest
+import exceptions
+
+from libcloud.drivers.vcloud import TerremarkDriver
+from libcloud.drivers.vcloud import VCloudNodeDriver
+from libcloud.base import Node, NodeImage, NodeSize
+from libcloud.types import NodeState
+
+from test import MockHttp, TestCaseMixin
+
+import httplib
+
+from secrets import TERREMARK_USER, TERREMARK_SECRET
+
+class TerremarkTests(unittest.TestCase, TestCaseMixin):
+
+    def setUp(self):
+        VCloudNodeDriver.connectionCls.host = "test"
+        VCloudNodeDriver.connectionCls.conn_classes = (None, TerremarkMockHttp) 
+        TerremarkMockHttp.type = None
+        self.driver = TerremarkDriver(TERREMARK_USER, TERREMARK_SECRET)
+
+    def test_list_images(self):
+        ret = self.driver.list_images()
+        self.assertEqual(ret[0].id,'https://services.vcloudexpress.terremark.com/api/v0.8/vAppTemplate/5')
+
+    def test_list_sizes(self):
+        ret = self.driver.list_sizes()
+        self.assertEqual(ret[0].ram, 512)
+        
+    def test_create_node(self):
+        image = self.driver.list_images()[0]
+        size = self.driver.list_sizes()[0]
+        node = self.driver.create_node(
+            name='testerpart2', 
+            image=image, 
+            size=size,
+            vdc='https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224',
+            network='https://services.vcloudexpress.terremark.com/api/v0.8/network/725', 
+            cpus=2,
+        )
+        self.assertTrue(isinstance(node, Node))
+        self.assertEqual(node.id, 'https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031')
+        self.assertEqual(node.name, 'testerpart2')
+        
+    def test_list_nodes(self):
+        ret = self.driver.list_nodes()
+        node = ret[0]
+        self.assertEqual(node.id, 'https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031')
+        self.assertEqual(node.name, 'testerpart2')
+        self.assertEqual(node.state, NodeState.RUNNING)
+        self.assertEqual(node.public_ip, [])
+        self.assertEqual(node.private_ip, ['10.112.78.69'])
+        
+    def test_reboot_node(self):
+        node = self.driver.list_nodes()[0]
+        ret = self.driver.reboot_node(node)
+        self.assertTrue(ret)
+        
+    def test_destroy_node(self):
+        node = self.driver.list_nodes()[0]
+        ret = self.driver.destroy_node(node)
+        self.assertTrue(ret)
+
+        
+class TerremarkMockHttp(MockHttp):
+
+    def _api_v0_8_login(self, method, url, body, headers):
+        headers['set-cookie'] = 'vcloud-token=testtoken'
+        body = """<OrgList xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Org href="https://services.vcloudexpress.terremark.com/api/v0.8/org/240" type="application/vnd.vmware.vcloud.org+xml" name="a@example.com"/>
+</OrgList>
+"""
+        return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+    def _api_v0_8_org_240(self, method, url, body, headers):
+        body = """<Org href="https://services.vcloudexpress.terremark.com/api/v0.8/org/240" name="a@example.com" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/catalog" type="application/vnd.vmware.vcloud.catalog+xml" name="Miami Environment 1 Catalog"/>
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/tasksList/224" type="application/vnd.vmware.vcloud.tasksList+xml" name="Miami Environment 1 Tasks List"/>
+</Org>
+"""
+        return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+    def _api_v0_8_vdc_224(self, method, url, body, headers):
+        body = """<Vdc href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/catalog" type="application/vnd.vmware.vcloud.catalog+xml" name="Miami Environment 1"/>
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/publicIps" type="application/xml" name="Public IPs"/>
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/internetServices" type="application/xml" name="Internet Services"/>
+  <Description/>
+  <ResourceEntities>
+    <ResourceEntity href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+  </ResourceEntities>
+  <AvailableNetworks>
+    <Network href="https://services.vcloudexpress.terremark.com/api/v0.8/network/725" type="application/vnd.vmware.vcloud.network+xml" name="10.112.78.64/26"/>
+  </AvailableNetworks>
+</Vdc>
+"""
+        return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+    def _api_v0_8_vdc_224_catalog(self, method, url, body, headers):
+        body = """<Catalog href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224/catalog" type="application/vnd.vmware.vcloud.catalog+xml" name="Miami Environment 1" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <CatalogItems>
+    <CatalogItem href="https://services.vcloudexpress.terremark.com/api/v0.8/catalogItem/5" type="application/vnd.vmware.vcloud.catalogItem+xml" name="CentOS 5.3 (32-bit)"/>
+  </CatalogItems>
+</Catalog>
+"""
+        return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+
+    def _api_v0_8_catalogItem_5(self, method, url, body, headers):
+        body = """<CatalogItem href="https://services.vcloudexpress.terremark.com/api/v0.8/catalogItem/5" type="application/vnd.vmware.vcloud.catalogItem+xml" name="CentOS 5.3 (32-bit)" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/catalogItem/5/options/compute" type="application/xml" name="Compute Options"/>
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/catalogItem/5/options/customization" type="application/xml" name="Customization Options"/>
+  <Entity href="https://services.vcloudexpress.terremark.com/api/v0.8/vAppTemplate/5" type="application/vnd.vmware.vcloud.vAppTemplate+xml" name="CentOS 5.3 (32-bit)"/>
+  <Property key="LicensingCost">0</Property>
+</CatalogItem>
+"""
+        return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+      
+    def _api_v0_8_vdc_224_action_instantiateVAppTemplate(self, method, url, body, headers):
+        body = """<VApp href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2" status="0" size="10" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Link rel="up" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml"/>
+</VApp>
+"""
+        return (httplib.OK, body, headers, httplib.responses[httplib.OK])
+      
+    def _api_v0_8_vapp_14031_action_deploy(self, method, url, body, headers):
+        body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/10496" type="application/vnd.vmware.vcloud.task+xml" status="queued" startTime="2009-11-13T23:58:22.893Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+  <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+        return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+      
+    def _api_v0_8_task_10496(self, method, url, body, headers):
+        body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/10496" type="application/vnd.vmware.vcloud.task+xml" status="success" startTime="2009-11-13T23:58:22.893Z" endTime="2009-11-14T00:01:02.507Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+  <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+        return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+      
+    def _api_v0_8_vapp_14031_power_action_powerOn(self, method, url, body, headers):
+        body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/10499" type="application/vnd.vmware.vcloud.task+xml" status="queued" startTime="2009-11-14T00:01:05.227Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+  <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+
+"""
+        return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+      
+    def _api_v0_8_vapp_14031(self, method, url, body, headers):
+        if method == 'GET':
+            body = """<VApp href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2" status="4" size="10485760" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Link rel="up" href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml"/>
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031/options/compute" type="application/xml" name="Compute Options"/>
+  <Link rel="down" href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031/options/customization" type="application/xml" name="Customization Options"/>
+  <NetworkConnectionSection xmlns="http://schemas.dmtf.org/ovf/envelope/1">
+    <NetworkConnection Network="Internal" xmlns="http://www.vmware.com/vcloud/v0.8">
+      <IpAddress>10.112.78.69</IpAddress>
+    </NetworkConnection>
+  </NetworkConnectionSection>
+  <OperatingSystemSection d2p1:id="25" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:d2p1="http://schemas.dmtf.org/ovf/envelope/1">
+    <Info>The kind of installed guest operating system</Info>
+    <Description>Red Hat Enterprise Linux 5 (32-bit)</Description>
+  </OperatingSystemSection>
+  <VirtualHardwareSection xmlns="http://schemas.dmtf.org/ovf/envelope/1">
+    <Info>Virtual Hardware</Info>
+    <System>
+      <AutomaticRecoveryAction xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <AutomaticShutdownAction xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <AutomaticStartupAction xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <AutomaticStartupActionDelay xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <AutomaticStartupActionSequenceNumber xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <ConfigurationDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <ConfigurationFile xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <ConfigurationID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <CreationTime xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <Description xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">Virtual Hardware Family</ElementName>
+      <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">0</InstanceID>
+      <LogDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <RecoveryFile xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <SnapshotDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <SuspendDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <SwapFileDataRoot xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"/>
+      <VirtualSystemIdentifier xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">testerpart2</VirtualSystemIdentifier>
+      <VirtualSystemType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">vmx-07</VirtualSystemType>
+    </System>
+    <Item>
+      <Address xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AddressOnParent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AllocationUnits xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">hertz * 10^6</AllocationUnits>
+      <AutomaticAllocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AutomaticDeallocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ConsumerVisibility xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Description xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">Number of Virtual CPUs</Description>
+      <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">2 virtual CPU(s)</ElementName>
+      <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">1</InstanceID>
+      <Limit xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <MappingBehavior xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <OtherResourceType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Parent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <PoolID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Reservation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ResourceSubType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ResourceType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">3</ResourceType>
+      <VirtualQuantity xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">2</VirtualQuantity>
+      <VirtualQuantityUnits xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">count</VirtualQuantityUnits>
+      <Weight xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+    </Item>
+    <Item>
+      <Address xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AddressOnParent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AllocationUnits xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">byte * 2^20</AllocationUnits>
+      <AutomaticAllocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AutomaticDeallocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ConsumerVisibility xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Description xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">Memory Size</Description>
+      <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">512MB of memory</ElementName>
+      <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">2</InstanceID>
+      <Limit xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <MappingBehavior xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <OtherResourceType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Parent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <PoolID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Reservation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ResourceSubType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ResourceType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">4</ResourceType>
+      <VirtualQuantity xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">512</VirtualQuantity>
+      <VirtualQuantityUnits xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">byte * 2^20</VirtualQuantityUnits>
+      <Weight xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+    </Item>
+    <Item>
+      <Address xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">0</Address>
+      <AddressOnParent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AllocationUnits xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AutomaticAllocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AutomaticDeallocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ConsumerVisibility xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Description xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">SCSI Controller</Description>
+      <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">SCSI Controller 0</ElementName>
+      <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">3</InstanceID>
+      <Limit xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <MappingBehavior xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <OtherResourceType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Parent xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <PoolID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Reservation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ResourceSubType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">lsilogic</ResourceSubType>
+      <ResourceType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">6</ResourceType>
+      <VirtualQuantity xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <VirtualQuantityUnits xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Weight xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+    </Item>
+    <Item>
+      <Address xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AddressOnParent xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">0</AddressOnParent>
+      <AllocationUnits xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AutomaticAllocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <AutomaticDeallocation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Caption xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ConsumerVisibility xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Description xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ElementName xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">Hard Disk 1</ElementName>
+      <HostResource xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">10485760</HostResource>
+      <InstanceID xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">9</InstanceID>
+      <Limit xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <MappingBehavior xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <OtherResourceType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Parent xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">3</Parent>
+      <PoolID xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Reservation xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ResourceSubType xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <ResourceType xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">17</ResourceType>
+      <VirtualQuantity xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData">10485760</VirtualQuantity>
+      <VirtualQuantityUnits xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+      <Weight xsi:nil="true" xmlns="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"/>
+    </Item>
+  </VirtualHardwareSection>
+</VApp>
+"""
+        elif method == 'DELETE':
+            body = ''
+        return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+    def _api_v0_8_vapp_14031_power_action_reset(self, method, url, body, headers):
+        body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/10555" type="application/vnd.vmware.vcloud.task+xml" status="queued" startTime="2009-11-14T00:54:50.417Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+  <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+        return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+    def _api_v0_8_vapp_14031_power_action_poweroff(self, method, url, body, headers):
+        body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/11001" type="application/vnd.vmware.vcloud.task+xml" status="queued" startTime="2009-11-16T18:18:02.82Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+  <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+        return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+    def _api_v0_8_task_11001(self, method, url, body, headers):
+        body = """<Task href="https://services.vcloudexpress.terremark.com/api/v0.8/task/11001" type="application/vnd.vmware.vcloud.task+xml" status="success" startTime="2009-11-16T18:18:02.82Z" endTime="2009-11-16T18:18:17.567Z" xmlns="http://www.vmware.com/vcloud/v0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Owner href="https://services.vcloudexpress.terremark.com/api/v0.8/vdc/224" type="application/vnd.vmware.vcloud.vdc+xml" name="Miami Environment 1"/>
+  <Result href="https://services.vcloudexpress.terremark.com/api/v0.8/vapp/14031" type="application/vnd.vmware.vcloud.vApp+xml" name="testerpart2"/>
+</Task>
+"""
+        return (httplib.ACCEPTED, body, headers, httplib.responses[httplib.ACCEPTED])
+
+      
diff --git a/test/test_voxel.py b/test/test_voxel.py
new file mode 100644
index 0000000..15dd0e9
--- /dev/null
+++ b/test/test_voxel.py
@@ -0,0 +1,50 @@
+# 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.
+# libcloud.org 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 unittest
+
+from libcloud.drivers.voxel import VoxelNodeDriver as Voxel
+from libcloud.types import Provider, NodeState, InvalidCredsException
+from libcloud.base import Node, NodeImage, NodeSize
+
+import httplib
+
+from test import MockHttp, multipleresponse, TestCaseMixin
+from secrets import VOXEL_USER, VOXEL_SECRET
+from xml.etree import ElementTree as ET
+
+class VoxelTest(unittest.TestCase):
+
+    def setUp(self):
+
+        Voxel.connectionCls.conn_classes = (None, VoxelMockHttp)
+        VoxelMockHttp.type = None
+        self.driver = Voxel('foo', 'bar')
+
+    def test_auth_failed(self):
+        VoxelMockHttp.type = 'UNAUTHORIZED'
+        try:
+            ret = self.driver.list_nodes()
+        except Exception, e:
+            self.assertTrue(isinstance(e, InvalidCredsException))
+        else:
+            self.fail('test should have thrown')
+
+class VoxelMockHttp(MockHttp):
+
+    def _UNAUTHORIZED(self, method, url, body, headers):
+        body = """<?xml version="1.0"?>
+<rsp stat="fail"><err code="1" msg="Invalid login or password"/><method>voxel.devices.list</method><parameters><param name="timestamp">2010-02-10T23:39:25.808107+0000</param><param name="key">authshouldfail</param><param name="api_sig">ae069bb835e998622caaddaeff8c98e0</param></parameters><string_to_sign>YOUR_SECRETtimestamp2010-02-10T23:39:25.808107+0000methodvoxel.devices.listkeyauthshouldfail</string_to_sign></rsp>
+"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
diff --git a/test/test_vpsnet.py b/test/test_vpsnet.py
new file mode 100644
index 0000000..52bd77c
--- /dev/null
+++ b/test/test_vpsnet.py
@@ -0,0 +1,205 @@
+# 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.
+# libcloud.org 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 unittest
+import exceptions
+
+from libcloud.drivers.vpsnet import VPSNetNodeDriver
+from libcloud.base import Node, NodeImage, NodeSize
+from libcloud.types import NodeState
+
+from test import MockHttp, TestCaseMixin
+
+import httplib
+
+from secrets import VPSNET_USER, VPSNET_KEY
+
+class VPSNetTests(unittest.TestCase, TestCaseMixin):
+
+    def setUp(self):
+        VPSNetNodeDriver.connectionCls.conn_classes = (None, VPSNetMockHttp)
+        self.driver = VPSNetNodeDriver(VPSNET_USER, VPSNET_KEY)
+
+    def test_create_node(self):
+        VPSNetMockHttp.type = 'create'
+        image = self.driver.list_images()[0]
+        size = self.driver.list_sizes()[0]
+        node = self.driver.create_node('foo', image, size)
+        self.assertEqual(node.name, 'foo')
+
+    def test_list_nodes(self):
+        VPSNetMockHttp.type = 'virtual_machines'
+        node = self.driver.list_nodes()[0]
+        self.assertEqual(node.id, 1384)
+        self.assertEqual(node.state, NodeState.RUNNING)
+
+    def test_reboot_node(self):
+        VPSNetMockHttp.type = 'virtual_machines'
+        node = self.driver.list_nodes()[0]
+
+        VPSNetMockHttp.type = 'reboot'
+        ret = self.driver.reboot_node(node)
+        self.assertEqual(ret, True)
+
+    def test_destroy_node(self):
+        VPSNetMockHttp.type = 'delete'
+        node = Node('2222', None, None, None, None, self.driver)
+        ret = self.driver.destroy_node(node)
+        self.assertTrue(ret)
+        VPSNetMockHttp.type = 'delete_fail'
+        node = Node('2223', None, None, None, None, self.driver)
+        self.assertRaises(exceptions.Exception, self.driver.destroy_node, node)
+
+    def test_list_images(self):
+        VPSNetMockHttp.type = 'templates'
+        ret = self.driver.list_images()
+        self.assertEqual(ret[0].id, 9)
+        self.assertEqual(ret[-1].id, 160)
+
+    def test_list_sizes(self):
+        VPSNetMockHttp.type = 'sizes'
+        ret = self.driver.list_sizes()
+        self.assertEqual(len(ret), 2)
+        self.assertEqual(ret[1].id, 2)
+        self.assertEqual(ret[1].name, '2 Node')
+        
+    def test_destroy_node_response(self):
+        # should return a node object
+        node = Node('2222', None, None, None, None, self.driver)
+        VPSNetMockHttp.type = 'delete'
+        ret = self.driver.destroy_node(node)
+        self.assertTrue(isinstance(ret, bool))
+
+    def test_reboot_node_response(self):
+        # should return a node object
+        VPSNetMockHttp.type = 'virtual_machines'
+        node = self.driver.list_nodes()[0]
+        VPSNetMockHttp.type = 'reboot'
+        ret = self.driver.reboot_node(node)
+        self.assertTrue(isinstance(ret, bool))
+
+
+
+class VPSNetMockHttp(MockHttp):
+
+
+    def _nodes_api10json_sizes(self, method, url, body, headers):
+        body = """[{"slice":{"virtual_machine_id":8592,"id":12256,"consumer_id":0}},
+                   {"slice":{"virtual_machine_id":null,"id":12258,"consumer_id":0}},
+                   {"slice":{"virtual_machine_id":null,"id":12434,"consumer_id":0}}]"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _nodes_api10json_create(self, method, url, body, headers):
+        body = """[{"slice":{"virtual_machine_id":8592,"id":12256,"consumer_id":0}},
+                   {"slice":{"virtual_machine_id":null,"id":12258,"consumer_id":0}},
+                   {"slice":{"virtual_machine_id":null,"id":12434,"consumer_id":0}}]"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _virtual_machines_2222_api10json_delete_fail(self, method, url, body, headers):
+        return (httplib.FORBIDDEN, '', {}, httplib.responses[httplib.FORBIDDEN])
+
+    def _virtual_machines_2222_api10json_delete(self, method, url, body, headers):
+        return (httplib.OK, '', {}, httplib.responses[httplib.OK])
+
+    def _virtual_machines_1384_reboot_api10json_reboot(self, method, url, body, headers):
+        body = """{
+              "virtual_machine": 
+                {
+                  "running": true, 
+                  "updated_at": "2009-05-15T06:55:02-04:00", 
+                  "power_action_pending": false, 
+                  "system_template_id": 41, 
+                  "id": 1384, 
+                  "cloud_id": 3, 
+                  "domain_name": "demodomain.com", 
+                  "hostname": "web01", 
+                  "consumer_id": 0, 
+                  "backups_enabled": false, 
+                  "password": "a8hjsjnbs91", 
+                  "label": "foo", 
+                  "slices_count": null, 
+                  "created_at": "2009-04-16T08:17:39-04:00"
+                }
+              }"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _virtual_machines_api10json_create(self, method, url, body, headers):
+        body = """{
+              "virtual_machine": 
+                {
+                  "running": true, 
+                  "updated_at": "2009-05-15T06:55:02-04:00", 
+                  "power_action_pending": false, 
+                  "system_template_id": 41, 
+                  "id": 1384, 
+                  "cloud_id": 3, 
+                  "domain_name": "demodomain.com", 
+                  "hostname": "web01", 
+                  "consumer_id": 0, 
+                  "backups_enabled": false, 
+                  "password": "a8hjsjnbs91", 
+                  "label": "foo", 
+                  "slices_count": null, 
+                  "created_at": "2009-04-16T08:17:39-04:00"
+                }
+              }"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _virtual_machines_api10json_virtual_machines(self, method, url, body, headers):
+        body = """     [{
+              "virtual_machine": 
+                {
+                  "running": true, 
+                  "updated_at": "2009-05-15T06:55:02-04:00", 
+                  "power_action_pending": false, 
+                  "system_template_id": 41, 
+                  "id": 1384, 
+                  "cloud_id": 3, 
+                  "domain_name": "demodomain.com", 
+                  "hostname": "web01", 
+                  "consumer_id": 0, 
+                  "backups_enabled": false, 
+                  "password": "a8hjsjnbs91", 
+                  "label": "Web Server 01", 
+                  "slices_count": null, 
+                  "created_at": "2009-04-16T08:17:39-04:00"
+                }
+              },
+              {
+                "virtual_machine": 
+                  {
+                    "running": true, 
+                    "updated_at": "2009-05-15T06:55:02-04:00", 
+                    "power_action_pending": false, 
+                    "system_template_id": 41, 
+                    "id": 1385, 
+                    "cloud_id": 3, 
+                    "domain_name": "demodomain.com", 
+                    "hostname": "mysql01", 
+                    "consumer_id": 0, 
+                    "backups_enabled": false, 
+                    "password": "dsi8h38hd2s", 
+                    "label": "MySQL Server 01", 
+                    "slices_count": null, 
+                    "created_at": "2009-04-16T08:17:39-04:00"
+                  }
+                }]"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+
+    def _available_clouds_api10json_templates(self, method, url, body, headers):
+        body = """[{"cloud":{"system_templates":[{"id":9,"label":"Ubuntu 8.04 x64"},{"id":10,"label":"CentOS 5.2 x64"},{"id":11,"label":"Gentoo 2008.0 x64"},{"id":18,"label":"Ubuntu 8.04 x64 LAMP"},{"id":19,"label":"Ubuntu 8.04 x64 MySQL"},{"id":20,"label":"Ubuntu 8.04 x64 Postfix"},{"id":21,"label":"Ubuntu 8.04 x64 Apache"},{"id":22,"label":"CentOS 5.2 x64 MySQL"},{"id":23,"label":"CentOS 5.2 x64 LAMP"},{"id":24,"label":"CentOS 5.2 x64 HAProxy"},{"id":25,"label":"CentOS 5.2 x64 Postfix"},{"id":26,"label":"CentOS 5.2 x64 Varnish"},{"id":27,"label":"CentOS 5.2 x64 Shoutcast"},{"id":28,"label":"CentOS 5.2 x64 Apache"},{"id":40,"label":"cPanel"},{"id":42,"label":"Debian 5.0 (Lenny) x64"},{"id":58,"label":"Django on Ubuntu 8.04 (x86)"},{"id":59,"label":"Drupal 5 on Ubuntu 8.04 (x86)"},{"id":60,"label":"Drupal 6 on Ubuntu 8.04 (x86)"},{"id":61,"label":"Google App Engine on Ubuntu 8.04 (x86)"},{"id":62,"label":"LAMP on Ubuntu 8.04 (x86)"},{"id":63,"label":"LAPP on Ubuntu 8.04 (x86)"},{"id":64,"label":"MediaWiki on Ubuntu 8.04 (x86)"},{"id":65,"label":"MySQL on Ubuntu 8.04 (x86)"},{"id":66,"label":"phpBB on Ubuntu 8.04 (x86)"},{"id":67,"label":"PostgreSQL on Ubuntu 8.04 (x86)"},{"id":68,"label":"Rails on Ubuntu 8.04 (x86)"},{"id":69,"label":"Tomcat on Ubuntu 8.04 (x86)"},{"id":70,"label":"Wordpress on Ubuntu 8.04 (x86)"},{"id":71,"label":"Joomla on Ubuntu 8.04 (x86)"},{"id":72,"label":"Ubuntu 8.04 Default Install (turnkey)"},{"id":128,"label":"CentOS Optimised"},{"id":129,"label":"Optimised CentOS + Apache + MySQL + PHP"},{"id":130,"label":"Optimised CentOS + Apache + MySQL + Ruby"},{"id":131,"label":"Optimised CentOS + Apache + MySQL + Ruby + PHP"},{"id":132,"label":"Debian Optimised"},{"id":133,"label":"Optimised Debian + Apache + MySQL + PHP"},{"id":134,"label":"Optimised Debian + NGINX + MySQL + PHP"},{"id":135,"label":"Optimised Debian + Lighttpd + MySQL + PHP"},{"id":136,"label":"Optimised Debian + Apache + MySQL + Ruby + PHP"},{"id":137,"label":"Optimised Debian + Apache + MySQL + Ruby"},{"id":138,"label":"Optimised Debian + NGINX + MySQL + Ruby + PHP"},{"id":139,"label":"Optimised Debian + NGINX + MySQL + Ruby"},{"id":140,"label":"Optimised Debian + Apache + MySQL + PHP + Magento"},{"id":141,"label":"Optimised Debian + NGINX + MySQL + PHP + Magento"},{"id":142,"label":"Optimised Debian + Lighttpd + MySQL + PHP + Wordpress"}],"id":2,"label":"USA VPS Cloud"}},{"cloud":{"system_templates":[{"id":15,"label":"Ubuntu 8.04 x64"},{"id":16,"label":"CentOS 5.2 x64"},{"id":17,"label":"Gentoo 2008.0 x64"},{"id":29,"label":"Ubuntu 8.04 x64 LAMP"},{"id":30,"label":"Ubuntu 8.04 x64 MySQL"},{"id":31,"label":"Ubuntu 8.04 x64 Postfix"},{"id":32,"label":"Ubuntu 8.04 x64 Apache"},{"id":33,"label":"CentOS 5.2 x64 MySQL"},{"id":34,"label":"CentOS 5.2 x64 LAMP"},{"id":35,"label":"CentOS 5.2 x64 HAProxy"},{"id":36,"label":"CentOS 5.2 x64 Postfix"},{"id":37,"label":"CentOS 5.2 x64 Varnish"},{"id":38,"label":"CentOS 5.2 x64 Shoutcast"},{"id":39,"label":"CentOS 5.2 x64 Apache"},{"id":41,"label":"cPanel"},{"id":43,"label":"Debian 5.0 (Lenny) x64"},{"id":44,"label":"Django on Ubuntu 8.04 (x86)"},{"id":45,"label":"Drupal 5 on Ubuntu 8.04 (x86)"},{"id":46,"label":"Drupal 6 on Ubuntu 8.04 (x86)"},{"id":47,"label":"Google App Engine on Ubuntu 8.04 (x86)"},{"id":48,"label":"LAMP on Ubuntu 8.04 (x86)"},{"id":49,"label":"LAPP on Ubuntu 8.04 (x86)"},{"id":50,"label":"MediaWiki on Ubuntu 8.04 (x86)"},{"id":51,"label":"MySQL on Ubuntu 8.04 (x86)"},{"id":52,"label":"phpBB on Ubuntu 8.04 (x86)"},{"id":53,"label":"PostgreSQL on Ubuntu 8.04 (x86)"},{"id":54,"label":"Rails on Ubuntu 8.04 (x86)"},{"id":55,"label":"Tomcat on Ubuntu 8.04 (x86)"},{"id":56,"label":"Wordpress on Ubuntu 8.04 (x86)"},{"id":57,"label":"Joomla on Ubuntu 8.04 (x86)"},{"id":73,"label":"Ubuntu 8.04 Default Install (turnkey)"},{"id":148,"label":"CentOS Optimised"},{"id":149,"label":"Optimised CentOS + Apache + MySQL + PHP"},{"id":150,"label":"Optimised CentOS + Apache + MySQL + Ruby"},{"id":151,"label":"Optimised CentOS + Apache + MySQL + Ruby + PHP"},{"id":152,"label":"Debian Optimised"},{"id":153,"label":"Optimised Debian + Apache + MySQL + PHP"},{"id":154,"label":"Optimised Debian + NGINX + MySQL + PHP"},{"id":155,"label":"Optimised Debian + Lighttpd + MySQL + PHP"},{"id":156,"label":"Optimised Debian + Apache + MySQL + Ruby + PHP"},{"id":157,"label":"Optimised Debian + Apache + MySQL + Ruby"},{"id":158,"label":"Optimised Debian + NGINX + MySQL + Ruby + PHP"},{"id":159,"label":"Optimised Debian + NGINX + MySQL + Ruby"},{"id":160,"label":"Optimised Debian + Lighttpd + MySQL + PHP + Wordpress"}],"id":3,"label":"UK VPS Cloud"}}]"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])
+    def _available_clouds_api10json_create(self, method, url, body, headers):
+        body = """[{"cloud":{"system_templates":[{"id":9,"label":"Ubuntu 8.04 x64"}],"id":2,"label":"USA VPS Cloud"}}]"""
+        return (httplib.OK, body, {}, httplib.responses[httplib.OK])