Import all the stacktester stuff as-is (s/stacktester/kong/, though).
diff --git a/kong/nova.py b/kong/nova.py
new file mode 100644
index 0000000..41b4b4e
--- /dev/null
+++ b/kong/nova.py
@@ -0,0 +1,155 @@
+import json
+import logging
+import subprocess
+
+import kong.common.http
+from kong import exceptions
+
+
+class API(kong.common.http.Client):
+    """Barebones Nova HTTP API client."""
+
+    def __init__(self, host, port, base_url, user, api_key, project_id=''):
+        """Initialize Nova HTTP API client.
+
+        :param host: Hostname/IP of the Nova API to test.
+        :param port: Port of the Nova API to test.
+        :param base_url: Version identifier (normally /v1.0 or /v1.1)
+        :param user: The username to use for tests.
+        :param api_key: The API key of the user.
+        :returns: None
+
+        """
+        super(API, self).__init__(host, port, base_url)
+        self.user = user
+        self.api_key = api_key
+        self.project_id = project_id
+        # Default to same as base_url, but will be change on auth
+        self.management_url = self.base_url
+
+    def authenticate(self, user, api_key, project_id):
+        """Request and return an authentication token from Nova.
+
+        :param user: The username we're authenticating.
+        :param api_key: The API key for the user we're authenticating.
+        :returns: Authentication token (string)
+        :raises: KeyError if authentication fails.
+
+        """
+        headers = {
+            'X-Auth-User': user,
+            'X-Auth-Key': api_key,
+            'X-Auth-Project-Id': project_id,
+        }
+        resp, body = super(API, self).request('GET', '', headers=headers,
+                                              base_url=self.base_url)
+
+        try:
+            self.management_url = resp['x-server-management-url']
+            return resp['x-auth-token']
+        except KeyError:
+            print "Failed to authenticate user"
+            raise
+
+    def _wait_for_entity_status(self, url, entity_name, status, **kwargs):
+        """Poll the provided url until expected entity status is returned"""
+
+        def check_response(resp, body):
+            try:
+                data = json.loads(body)
+                return data[entity_name]['status'] == status
+            except (ValueError, KeyError):
+                return False
+
+        try:
+            self.poll_request('GET', url, check_response, **kwargs)
+        except exceptions.TimeoutException:
+            msg = "%s failed to reach status %s" % (entity_name, status)
+            raise AssertionError(msg)
+
+    def wait_for_server_status(self, server_id, status='ACTIVE', **kwargs):
+        """Wait for the server status to be equal to the status passed in.
+
+        :param server_id: Server ID to query.
+        :param status: The status string to look for.
+        :returns: None
+        :raises: AssertionError if request times out
+
+        """
+        url = '/servers/%s' % server_id
+        return self._wait_for_entity_status(url, 'server', status, **kwargs)
+
+    def wait_for_image_status(self, image_id, status='ACTIVE', **kwargs):
+        """Wait for the image status to be equal to the status passed in.
+
+        :param image_id: Image ID to query.
+        :param status: The status string to look for.
+        :returns: None
+        :raises: AssertionError if request times out
+
+        """
+        url = '/images/%s' % image_id
+        return self._wait_for_entity_status(url, 'image', status, **kwargs)
+
+    def request(self, method, url, **kwargs):
+        """Generic HTTP request on the Nova API.
+
+        :param method: Request verb to use (GET, PUT, POST, etc.)
+        :param url: The API resource to request.
+        :param kwargs: Additional keyword arguments to pass to the request.
+        :returns: HTTP response object.
+
+        """
+        headers = kwargs.get('headers', {})
+        project_id = kwargs.get('project_id', self.project_id)
+
+        headers['X-Auth-Token'] = self.authenticate(self.user, self.api_key,
+                                                    self.project_id)
+        kwargs['headers'] = headers
+        return super(API, self).request(method, url, **kwargs)
+
+    def get_server(self, server_id):
+        """Fetch a server by id
+
+        :param server_id: dict of server attributes
+        :returns: dict of server attributes
+        :raises: ServerNotFound if server does not exist
+
+        """
+        resp, body = self.request('GET', '/servers/%s' % server_id)
+        try:
+            assert resp['status'] == '200'
+            data = json.loads(body)
+            return data['server']
+        except (AssertionError, ValueError, TypeError, KeyError):
+            raise exceptions.ServerNotFound(server_id)
+
+    def create_server(self, entity):
+        """Attempt to create a new server.
+
+        :param entity: dict of server attributes
+        :returns: dict of server attributes after creation
+        :raises: AssertionError if server creation fails
+
+        """
+        post_body = json.dumps({
+            'server': entity,
+        })
+
+        resp, body = self.request('POST', '/servers', body=post_body)
+        try:
+            assert resp['status'] == '202'
+            data = json.loads(body)
+            return data['server']
+        except (AssertionError, ValueError, TypeError, KeyError):
+            raise AssertionError("Failed to create server")
+
+    def delete_server(self, server_id):
+        """Attempt to delete a server.
+
+        :param server_id: server identifier
+        :returns: None
+
+        """
+        url = '/servers/%s' % server_id
+        response, body = self.request('DELETE', url)