Merge "Enable boto keyapir test"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index e1196f0..43277d6 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -107,9 +107,6 @@
 # Level to log Compute API request/response details.
 log_level = ERROR
 
-# Whitebox options for compute. Whitebox options enable the
-# whitebox test cases, which look at internal Nova database state,
-# SSH into VMs to check instance state, etc.
 
 # Run live migration tests (requires 2 hosts)
 live_migration_available = false
@@ -123,6 +120,12 @@
 # are forced to skip, regardless of the extension status
 disk_config_enabled_override = true
 
+
+[whitebox]
+# Whitebox options for compute. Whitebox options enable the
+# whitebox test cases, which look at internal Nova database state,
+# SSH into VMs to check instance state, etc.
+
 # Should we run whitebox tests for Compute?
 whitebox_enabled = true
 
diff --git a/openstack-common.conf b/openstack-common.conf
index a75279f..0c9e43e 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -1,7 +1,7 @@
 [DEFAULT]
 
 # The list of modules to copy from openstack-common
-modules=setup,cfg,iniparser
+modules=setup,cfg,iniparser,install_venv_common
 
 # The base module to hold the copy of openstack.common
 base=tempest
diff --git a/setup.cfg b/setup.cfg
index c28d129..b522f3a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,7 +1,5 @@
 [nosetests]
 # NOTE(jkoelker) To run the test suite under nose install the following
 #                coverage http://pypi.python.org/pypi/coverage
-#                tissue http://pypi.python.org/pypi/tissue (pep8 checker)
 #                openstack-nose https://github.com/openstack-dev/openstack-nose
 verbosity=2
-detailed-errors=1
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index cff038d..d649930 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -199,6 +199,34 @@
     def _parse_resp(self, body):
         return json.loads(body)
 
+    def response_checker(self, method, url, headers, body, resp, resp_body):
+        if (resp.status in set((204, 205, 304)) or resp.status < 200 or
+            method.upper() == 'HEAD') and resp_body:
+            raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
+        #NOTE(afazekas):
+        # If the HTTP Status Code is 205
+        #   'The response MUST NOT include an entity.'
+        # A HTTP entity has an entity-body and an 'entity-header'.
+        # In the HTTP response specification (Section 6) the 'entity-header'
+        # 'generic-header' and 'response-header' are in OR relation.
+        # All headers not in the above two group are considered as entity
+        # header in every interpretation.
+
+        if (resp.status == 205 and
+            0 != len(set(resp.keys()) - set(('status',)) -
+                     self.response_header_lc - self.general_header_lc)):
+                        raise exceptions.ResponseWithEntity()
+        #NOTE(afazekas)
+        # Now the swift sometimes (delete not empty container)
+        # returns with non json error response, we can create new rest class
+        # for swift.
+        # Usually RFC2616 says error responses SHOULD contain an explanation.
+        # The warning is normal for SHOULD/SHOULD NOT case
+
+        # Likely it will cause error
+        if not body and resp.status >= 400:
+            self.log.warning("status >= 400 response with empty body")
+
     def request(self, method, url,
                 headers=None, body=None, depth=0, wait=None):
         """A simple HTTP request interface."""
@@ -215,37 +243,7 @@
         req_url = "%s/%s" % (self.base_url, url)
         resp, resp_body = self.http_obj.request(req_url, method,
                                                 headers=headers, body=body)
-
-        #TODO(afazekas): Make sure we can validate all responses, and the
-        #http library does not do any action automatically
-        if (resp.status in set((204, 205, 304)) or resp.status < 200 or
-                method.upper() == 'HEAD') and resp_body:
-                raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
-
-        #NOTE(afazekas):
-        # If the HTTP Status Code is 205
-        #   'The response MUST NOT include an entity.'
-        # A HTTP entity has an entity-body and an 'entity-header'.
-        # In the HTTP response specification (Section 6) the 'entity-header'
-        # 'generic-header' and 'response-header' are in OR relation.
-        # All headers not in the above two group are considered as entity
-        # header in every interpretation.
-
-        if (resp.status == 205 and
-            0 != len(set(resp.keys()) - set(('status',)) -
-                     self.response_header_lc - self.general_header_lc)):
-                        raise exceptions.ResponseWithEntity()
-
-        #NOTE(afazekas)
-        # Now the swift sometimes (delete not empty container)
-        # returns with non json error response, we can create new rest class
-        # for swift.
-        # Usually RFC2616 says error responses SHOULD contain an explanation.
-        # The warning is normal for SHOULD/SHOULD NOT case
-
-        # Likely it will cause error
-        if not body and resp.status >= 400:
-            self.log.warning("status >= 400 response with empty body")
+        self.response_checker(method, url, headers, body, resp, resp_body)
 
         if resp.status == 401 or resp.status == 403:
             self._log(req_url, body, resp, resp_body)
@@ -296,7 +294,7 @@
         if resp.status >= 400:
             resp_body = self._parse_resp(resp_body)
             self._log(req_url, body, resp, resp_body)
-            raise exceptions.TempestException(str(resp.status))
+            raise exceptions.RestClientException(str(resp.status))
 
         return resp, resp_body
 
@@ -344,7 +342,9 @@
         """
         Subclasses override with specific deletion detection.
         """
-        return False
+        message = ('"%s" does not implement is_resource_deleted'
+                   % self.__class__.__name__)
+        raise NotImplementedError(message)
 
 
 class RestClientXML(RestClient):
diff --git a/tempest/common/utils/misc.py b/tempest/common/utils/misc.py
new file mode 100644
index 0000000..8d945cf
--- /dev/null
+++ b/tempest/common/utils/misc.py
@@ -0,0 +1,27 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+
+def singleton(cls):
+    """Simple wrapper for classes that should only have a single instance."""
+    instances = {}
+
+    def getinstance():
+        if cls not in instances:
+            instances[cls] = cls()
+        return instances[cls]
+    return getinstance
diff --git a/tempest/config.py b/tempest/config.py
index 89fa2d9..f6fcd90 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -20,6 +20,7 @@
 import sys
 
 from tempest.common.utils import data_utils
+from tempest.common.utils.misc import singleton
 from tempest.openstack.common import cfg
 
 LOG = logging.getLogger(__name__)
@@ -171,22 +172,6 @@
                 default=True,
                 help="If false, skip config tests regardless of the "
                      "extension status"),
-    cfg.BoolOpt('whitebox_enabled',
-                default=False,
-                help="Does the test environment support whitebox tests for "
-                     "Compute?"),
-    cfg.StrOpt('db_uri',
-               default=None,
-               help="Connection string to the database of Compute service"),
-    cfg.StrOpt('source_dir',
-               default="/opt/stack/nova",
-               help="Path of nova source directory"),
-    cfg.StrOpt('config_path',
-               default='/etc/nova/nova.conf',
-               help="Path of nova configuration file"),
-    cfg.StrOpt('bin_dir',
-               default="/usr/local/bin/",
-               help="Directory containing nova binaries such as nova-manage"),
 ]
 
 
@@ -219,6 +204,35 @@
         conf.register_opt(opt, group='compute-admin')
 
 
+whitebox_group = cfg.OptGroup(name='whitebox',
+                              title="Whitebox Options")
+
+WhiteboxGroup = [
+    cfg.BoolOpt('whitebox_enabled',
+                default=False,
+                help="Does the test environment support whitebox tests for "
+                     "Compute?"),
+    cfg.StrOpt('db_uri',
+               default=None,
+               help="Connection string to the database of Compute service"),
+    cfg.StrOpt('source_dir',
+               default="/opt/stack/nova",
+               help="Path of nova source directory"),
+    cfg.StrOpt('config_path',
+               default='/etc/nova/nova.conf',
+               help="Path of nova configuration file"),
+    cfg.StrOpt('bin_dir',
+               default="/usr/local/bin/",
+               help="Directory containing nova binaries such as nova-manage"),
+]
+
+
+def register_whitebox_opts(conf):
+    conf.register_group(whitebox_group)
+    for opt in WhiteboxGroup:
+        conf.register_opt(opt, group='whitebox')
+
+
 image_group = cfg.OptGroup(name='image',
                            title="Image Service Options")
 
@@ -370,18 +384,6 @@
         conf.register_opt(opt, group='boto')
 
 
-# TODO(jaypipes): Move this to a common utils (not data_utils...)
-def singleton(cls):
-    """Simple wrapper for classes that should only have a single instance."""
-    instances = {}
-
-    def getinstance():
-        if cls not in instances:
-            instances[cls] = cls()
-        return instances[cls]
-    return getinstance
-
-
 @singleton
 class TempestConfig:
     """Provides OpenStack configuration information."""
@@ -418,6 +420,7 @@
 
         register_compute_opts(cfg.CONF)
         register_identity_opts(cfg.CONF)
+        register_whitebox_opts(cfg.CONF)
         register_image_opts(cfg.CONF)
         register_network_opts(cfg.CONF)
         register_volume_opts(cfg.CONF)
@@ -425,6 +428,7 @@
         register_boto_opts(cfg.CONF)
         register_compute_admin_opts(cfg.CONF)
         self.compute = cfg.CONF.compute
+        self.whitebox = cfg.CONF.whitebox
         self.identity = cfg.CONF.identity
         self.images = cfg.CONF.image
         self.network = cfg.CONF.network
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 178c2f2..de22688 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -15,6 +15,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import unittest2 as unittest
+
 
 class TempestException(Exception):
     """
@@ -27,6 +29,7 @@
     message = "An unknown exception occurred"
 
     def __init__(self, *args, **kwargs):
+        super(TempestException, self).__init__()
         try:
             self._error_string = self.message % kwargs
         except Exception:
@@ -49,11 +52,16 @@
     message = "Invalid Configuration"
 
 
-class NotFound(TempestException):
+class RestClientException(TempestException,
+                          unittest.TestCase.failureException):
+    pass
+
+
+class NotFound(RestClientException):
     message = "Object not found"
 
 
-class Unauthorized(TempestException):
+class Unauthorized(RestClientException):
     message = 'Unauthorized'
 
 
@@ -78,11 +86,11 @@
     message = "Volume %(volume_id)s failed to build and is in ERROR status"
 
 
-class BadRequest(TempestException):
+class BadRequest(RestClientException):
     message = "Bad request"
 
 
-class AuthenticationFailure(TempestException):
+class AuthenticationFailure(RestClientException):
     message = ("Authentication with user %(user)s and password "
                "%(password)s failed")
 
@@ -108,7 +116,7 @@
     message = "Got identity error"
 
 
-class Duplicate(TempestException):
+class Duplicate(RestClientException):
     message = "An object with that identifier already exists"
 
 
@@ -135,7 +143,7 @@
     message = "%(num)d cleanUp operation failed"
 
 
-class RFCViolation(TempestException):
+class RFCViolation(RestClientException):
     message = "RFC Violation"
 
 
diff --git a/tempest/services/identity/json/identity_client.py b/tempest/services/identity/json/identity_client.py
index 9cc30d9..403a3ac 100644
--- a/tempest/services/identity/json/identity_client.py
+++ b/tempest/services/identity/json/identity_client.py
@@ -197,6 +197,12 @@
         body = json.loads(body)
         return resp, body['OS-KSADM:service']
 
+    def list_services(self):
+        """List Service - Returns Services."""
+        resp, body = self.get('/OS-KSADM/services/')
+        body = json.loads(body)
+        return resp, body['OS-KSADM:services']
+
     def delete_service(self, service_id):
         """Delete Service."""
         url = '/OS-KSADM/services/%s' % service_id
diff --git a/tempest/services/identity/xml/identity_client.py b/tempest/services/identity/xml/identity_client.py
index 02be91e..f79c3d5 100644
--- a/tempest/services/identity/xml/identity_client.py
+++ b/tempest/services/identity/xml/identity_client.py
@@ -226,6 +226,12 @@
         body = self._parse_body(etree.fromstring(body))
         return resp, body
 
+    def list_services(self):
+        """Returns services."""
+        resp, body = self.get('OS-KSADM/services', self.headers)
+        body = self._parse_array(etree.fromstring(body))
+        return resp, body
+
     def get_service(self, service_id):
         """Get Service."""
         url = '/OS-KSADM/services/%s' % service_id
diff --git a/tempest/services/volume/xml/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
index b0104e0..440a80e 100644
--- a/tempest/services/volume/xml/volumes_client.py
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -37,7 +37,7 @@
     def __init__(self, config, username, password, auth_url, tenant_name=None):
         super(VolumesClientXML, self).__init__(config, username, password,
                                                auth_url, tenant_name)
-        self.service = self.config.compute.catalog_type
+        self.service = self.config.volume.catalog_type
         self.build_interval = self.config.compute.build_interval
         self.build_timeout = self.config.compute.build_timeout
 
@@ -50,10 +50,11 @@
                 ns, tag = tag.split("}", 1)
             if tag == 'metadata':
                 vol['metadata'] = dict((meta.get('key'),
-                                        meta.text) for meta in list(child))
+                                       meta.text) for meta in
+                                       child.getchildren())
             else:
                 vol[tag] = xml_to_json(child)
-            return vol
+        return vol
 
     def list_volumes(self, params=None):
         """List all the volumes created."""
@@ -90,25 +91,33 @@
         body = etree.fromstring(body)
         return resp, self._parse_volume(body)
 
-    def create_volume(self, size, display_name=None, metadata=None):
+    def create_volume(self, size, **kwargs):
         """Creates a new Volume.
 
         :param size: Size of volume in GB. (Required)
         :param display_name: Optional Volume Name.
         :param metadata: An optional dictionary of values for metadata.
+        :param volume_type: Optional Name of volume_type for the volume
+        :param snapshot_id: When specified the volume is created from
+                            this snapshot
         """
         volume = Element("volume", xmlns=XMLNS_11, size=size)
-        if display_name:
-            volume.add_attr('display_name', display_name)
 
-        if metadata:
+        if 'metadata' in kwargs:
             _metadata = Element('metadata')
             volume.append(_metadata)
-            for key, value in metadata.items():
+            for key, value in kwargs['metadata'].items():
                 meta = Element('meta')
                 meta.add_attr('key', key)
                 meta.append(Text(value))
                 _metadata.append(meta)
+            attr_to_add = kwargs.copy()
+            del attr_to_add['metadata']
+        else:
+            attr_to_add = kwargs
+
+        for key, value in attr_to_add.items():
+            volume.add_attr(key, value)
 
         resp, body = self.post('volumes', str(Document(volume)),
                                self.headers)
@@ -122,7 +131,6 @@
     def wait_for_volume_status(self, volume_id, status):
         """Waits for a Volume to reach a given status."""
         resp, body = self.get_volume(volume_id)
-        volume_name = body['displayName']
         volume_status = body['status']
         start = int(time.time())
 
@@ -135,7 +143,8 @@
 
             if int(time.time()) - start >= self.build_timeout:
                 message = 'Volume %s failed to reach %s status within '\
-                          'the required time (%s s).' % (volume_name, status,
+                          'the required time (%s s).' % (volume_id,
+                                                         status,
                                                          self.build_timeout)
                 raise exceptions.TimeoutException(message)
 
diff --git a/tempest/testboto.py b/tempest/testboto.py
index 7031fe2..8bcaa33 100644
--- a/tempest/testboto.py
+++ b/tempest/testboto.py
@@ -126,7 +126,7 @@
                    testresources.ResourcedTestCase):
     """Recommended to use as base class for boto related test."""
 
-    resources = [('boto_init', tempest.tests.boto.BotoResource())]
+    conclusion = tempest.tests.boto.generic_setup_package()
 
     @classmethod
     def setUpClass(cls):
@@ -202,8 +202,10 @@
     s3_error_code.server = ServerError()
     s3_error_code.client = ClientError()
     valid_image_state = set(('available', 'pending', 'failed'))
+    #NOTE(afazekas): 'paused' is not valid status in EC2, but it does not have
+    # a good mapping, because it uses memory, but not really a running machine
     valid_instance_state = set(('pending', 'running', 'shutting-down',
-                                'terminated', 'stopping', 'stopped'))
+                                'terminated', 'stopping', 'stopped', 'paused'))
     valid_volume_status = set(('creating', 'available', 'in-use',
                                'deleting', 'deleted', 'error'))
     valid_snapshot_status = set(('pending', 'completed', 'error'))
diff --git a/tempest/tests/boto/__init__.py b/tempest/tests/boto/__init__.py
index 99dd8a9..6d5149e 100644
--- a/tempest/tests/boto/__init__.py
+++ b/tempest/tests/boto/__init__.py
@@ -26,7 +26,6 @@
 import tempest.clients
 from tempest.common.utils.file_utils import have_effective_read_access
 import tempest.config
-from testresources import TestResourceManager
 
 A_I_IMAGES_READY = False  # ari,ami,aki
 S3_CAN_CONNECT_ERROR = "Unknown Error"
@@ -97,8 +96,3 @@
     else:
         S3_CAN_CONNECT_ERROR = None
     boto_logger.setLevel(level)
-
-
-class BotoResource(TestResourceManager):
-    def make(self, dependency_resources=None):
-        return generic_setup_package()
diff --git a/tempest/tests/compute/__init__.py b/tempest/tests/compute/__init__.py
index 08e5091..5b59a70 100644
--- a/tempest/tests/compute/__init__.py
+++ b/tempest/tests/compute/__init__.py
@@ -29,7 +29,7 @@
 CREATE_IMAGE_ENABLED = CONFIG.compute.create_image_enabled
 RESIZE_AVAILABLE = CONFIG.compute.resize_available
 CHANGE_PASSWORD_AVAILABLE = CONFIG.compute.change_password_available
-WHITEBOX_ENABLED = CONFIG.compute.whitebox_enabled
+WHITEBOX_ENABLED = CONFIG.whitebox.whitebox_enabled
 DISK_CONFIG_ENABLED = False
 DISK_CONFIG_ENABLED_OVERRIDE = CONFIG.compute.disk_config_enabled_override
 FLAVOR_EXTRA_DATA_ENABLED = False
diff --git a/tempest/tests/compute/servers/test_server_addresses.py b/tempest/tests/compute/servers/test_server_addresses.py
index 6e819a2..6b0f7ae 100644
--- a/tempest/tests/compute/servers/test_server_addresses.py
+++ b/tempest/tests/compute/servers/test_server_addresses.py
@@ -74,9 +74,9 @@
 
         # We do not know the exact network configuration, but an instance
         # should at least have a single public or private address
-        self.assertGreaterEqual(len(addresses), 1)
+        self.assertTrue(len(addresses) >= 1)
         for network_name, network_addresses in addresses.iteritems():
-            self.assertGreaterEqual(len(network_addresses), 1)
+            self.assertTrue(len(network_addresses) >= 1)
             for address in network_addresses:
                 self.assertTrue(address['addr'])
                 self.assertTrue(address['version'])
diff --git a/tempest/tests/identity/admin/test_services.py b/tempest/tests/identity/admin/test_services.py
index 5261b9d..73f4a90 100644
--- a/tempest/tests/identity/admin/test_services.py
+++ b/tempest/tests/identity/admin/test_services.py
@@ -16,6 +16,9 @@
 #    under the License.
 
 
+from nose.plugins.attrib import attr
+import unittest2 as unittest
+
 from tempest.common.utils.data_utils import rand_name
 from tempest import exceptions
 from tempest.tests.identity import base
@@ -30,8 +33,8 @@
             name = rand_name('service-')
             type = rand_name('type--')
             description = rand_name('description-')
-            resp, service_data = \
-            self.client.create_service(name, type, description=description)
+            resp, service_data = self.client.create_service(
+                name, type, description=description)
             self.assertTrue(resp['status'].startswith('2'))
             #Verifying response body of create service
             self.assertTrue('id' in service_data)
@@ -63,6 +66,32 @@
             self.assertRaises(exceptions.NotFound, self.client.get_service,
                               service_data['id'])
 
+    def test_list_services(self):
+        # Create, List, Verify and Delete Services
+        services = []
+        for _ in xrange(3):
+            name = rand_name('service-')
+            type = rand_name('type--')
+            description = rand_name('description-')
+            resp, service = self.client.create_service(
+                name, type, description=description)
+            services.append(service)
+        service_ids = map(lambda x: x['id'], services)
+
+        # List and Verify Services
+        resp, body = self.client.list_services()
+        self.assertTrue(resp['status'].startswith('2'))
+        found = [service for service in body if service['id'] in service_ids]
+        self.assertEqual(len(found), len(services), 'Services not found')
+
+        # Delete Services
+        for service in services:
+            resp, body = self.client.delete_service(service['id'])
+            self.assertTrue(resp['status'].startswith('2'))
+        resp, body = self.client.list_services()
+        found = [service for service in body if service['id'] in service_ids]
+        self.assertFalse(any(found), 'Services failed to delete')
+
 
 class ServicesTestJSON(base.BaseIdentityAdminTestJSON, ServicesTestBase):
     @classmethod
diff --git a/tempest/tests/network/test_network_basic_ops.py b/tempest/tests/network/test_network_basic_ops.py
index bdebced..d297c98 100644
--- a/tempest/tests/network/test_network_basic_ops.py
+++ b/tempest/tests/network/test_network_basic_ops.py
@@ -186,9 +186,9 @@
         cls.check_preconditions()
         cfg = cls.config.network
         cls.tenant_id = cls.manager._get_identity_client(
-            cfg.username,
-            cfg.password,
-            cfg.tenant_name).tenant_id
+            cls.config.identity.username,
+            cls.config.identity.password,
+            cls.config.identity.tenant_name).tenant_id
         # TODO(mnewby) Consider looking up entities as needed instead
         # of storing them as collections on the class.
         cls.keypairs = {}
diff --git a/tempest/tests/object_storage/test_container_sync.py b/tempest/tests/object_storage/test_container_sync.py
index 597fd86..d612880 100644
--- a/tempest/tests/object_storage/test_container_sync.py
+++ b/tempest/tests/object_storage/test_container_sync.py
@@ -103,7 +103,7 @@
             self.assertEqual(resp['status'], '200',
                              'Error listing the destination container`s'
                              ' "%s" contents' % (self.containers[0]))
-            object_list_0 = {obj['name']: obj for obj in object_list_0}
+            object_list_0 = dict((obj['name'], obj) for obj in object_list_0)
             # get second container content
             resp, object_list_1 = \
                 cont_client[1].\
@@ -111,7 +111,7 @@
             self.assertEqual(resp['status'], '200',
                              'Error listing the destination container`s'
                              ' "%s" contents' % (self.containers[1]))
-            object_list_1 = {obj['name']: obj for obj in object_list_1}
+            object_list_1 = dict((obj['name'], obj) for obj in object_list_1)
             # check that containers is not empty and has equal keys()
             # or wait for next attepmt
             if not object_list_0 or not object_list_1 or \
diff --git a/tempest/tests/volume/admin/test_volume_types.py b/tempest/tests/volume/admin/test_volume_types.py
index cef8428..7184f11 100644
--- a/tempest/tests/volume/admin/test_volume_types.py
+++ b/tempest/tests/volume/admin/test_volume_types.py
@@ -21,6 +21,7 @@
 
 
 class VolumeTypesTest(BaseVolumeTest):
+    _interface = "json"
 
     @classmethod
     def setUpClass(cls):
diff --git a/tempest/tests/volume/admin/test_volume_types_extra_specs.py b/tempest/tests/volume/admin/test_volume_types_extra_specs.py
index d3fd8e5..c743567 100644
--- a/tempest/tests/volume/admin/test_volume_types_extra_specs.py
+++ b/tempest/tests/volume/admin/test_volume_types_extra_specs.py
@@ -21,6 +21,7 @@
 
 
 class VolumeTypesExtraSpecsTest(BaseVolumeTest):
+    _interface = "json"
 
     @classmethod
     def setUpClass(cls):
diff --git a/tempest/tests/volume/base.py b/tempest/tests/volume/base.py
index de78c99..32c211e 100644
--- a/tempest/tests/volume/base.py
+++ b/tempest/tests/volume/base.py
@@ -43,9 +43,10 @@
             username, tenant_name, password = creds
             os = clients.Manager(username=username,
                                  password=password,
-                                 tenant_name=tenant_name)
+                                 tenant_name=tenant_name,
+                                 interface=cls._interface)
         else:
-            os = clients.Manager()
+            os = clients.Manager(interface=cls._interface)
 
         cls.os = os
         cls.volumes_client = os.volumes_client
diff --git a/tempest/tests/volume/test_volumes_actions.py b/tempest/tests/volume/test_volumes_actions.py
index 155acb6..dd93b89 100644
--- a/tempest/tests/volume/test_volumes_actions.py
+++ b/tempest/tests/volume/test_volumes_actions.py
@@ -22,6 +22,7 @@
 
 
 class VolumesActionsTest(BaseVolumeTest):
+    _interface = "json"
 
     @classmethod
     def setUpClass(cls):
diff --git a/tempest/whitebox.py b/tempest/whitebox.py
index 03ad63b..b7a1e68 100644
--- a/tempest/whitebox.py
+++ b/tempest/whitebox.py
@@ -63,9 +63,9 @@
         super(ComputeWhiteboxTest, cls).setUpClass()
 
         # Add some convenience attributes that tests use...
-        cls.nova_dir = cls.config.compute.source_dir
-        cls.compute_bin_dir = cls.config.compute.bin_dir
-        cls.compute_config_path = cls.config.compute.config_path
+        cls.nova_dir = cls.config.whitebox.source_dir
+        cls.compute_bin_dir = cls.config.whitebox.bin_dir
+        cls.compute_config_path = cls.config.whitebox.config_path
         cls.servers_client = cls.manager.servers_client
         cls.images_client = cls.manager.images_client
         cls.flavors_client = cls.manager.flavors_client
@@ -126,7 +126,7 @@
                        }
 
         try:
-            engine = create_engine(cls.config.compute.db_uri, **engine_args)
+            engine = create_engine(cls.config.whitebox.db_uri, **engine_args)
             connection = engine.connect()
             meta = MetaData()
             meta.reflect(bind=engine)
diff --git a/tools/install_venv.py b/tools/install_venv.py
index 28275ba..52dbe39 100644
--- a/tools/install_venv.py
+++ b/tools/install_venv.py
@@ -5,6 +5,7 @@
 # All Rights Reserved.
 #
 # Copyright 2010 OpenStack, LLC
+# Copyright 2013 IBM Corp.
 #
 #    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
@@ -25,123 +26,10 @@
 import subprocess
 import sys
 
-
-ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-VENV = os.path.join(ROOT, '.venv')
-PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires')
-TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires')
-PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
+import install_venv_common as install_venv
 
 
-def die(message, *args):
-    print >> sys.stderr, message % args
-    sys.exit(1)
-
-
-def check_python_version():
-    if sys.version_info < (2, 6):
-        die("Need Python Version >= 2.6")
-
-
-def run_command_with_code(cmd, redirect_output=True, check_exit_code=True):
-    """Runs a command in an out-of-process shell.
-
-    Returns the output of that command.  Working directory is ROOT.
-    """
-    if redirect_output:
-        stdout = subprocess.PIPE
-    else:
-        stdout = None
-
-    proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout)
-    output = proc.communicate()[0]
-    if check_exit_code and proc.returncode != 0:
-        die('Command "%s" failed.\n%s', ' '.join(cmd), output)
-    return (output, proc.returncode)
-
-
-def run_command(cmd, redirect_output=True, check_exit_code=True):
-    return run_command_with_code(cmd, redirect_output, check_exit_code)[0]
-
-
-class Distro(object):
-
-    def check_cmd(self, cmd):
-        return bool(run_command(['which', cmd], check_exit_code=False).strip())
-
-    def install_virtualenv(self):
-        if self.check_cmd('virtualenv'):
-            return
-
-        if self.check_cmd('easy_install'):
-            print 'Installing virtualenv via easy_install...',
-            if run_command(['easy_install', 'virtualenv']):
-                print 'Succeeded'
-                return
-            else:
-                print 'Failed'
-
-        die('ERROR: virtualenv not found.\n\nTempest development'
-            ' requires virtualenv, please install it using your'
-            ' favorite package management tool')
-
-    def post_process(self):
-        """Any distribution-specific post-processing gets done here.
-
-        In particular, this is useful for applying patches to code inside
-        the venv.
-        """
-        pass
-
-
-class Fedora(Distro):
-    """This covers Fedora-based distributions.
-
-    Includes: Fedora, RHEL, Scientific Linux"""
-
-    def check_pkg(self, pkg):
-        return run_command_with_code(['rpm', '-q', pkg],
-                                     check_exit_code=False)[1] == 0
-
-    def yum_install(self, pkg, **kwargs):
-        print "Attempting to install '%s' via yum" % pkg
-        run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
-
-    def apply_patch(self, originalfile, patchfile):
-        run_command(['patch', originalfile, patchfile])
-
-    def install_virtualenv(self):
-        if self.check_cmd('virtualenv'):
-            return
-
-        if not self.check_pkg('python-virtualenv'):
-            self.yum_install('python-virtualenv', check_exit_code=False)
-
-        super(Fedora, self).install_virtualenv()
-
-    def post_process(self):
-        """Workaround for a bug in eventlet.
-
-        This currently affects RHEL6.1, but the fix can safely be
-        applied to all RHEL and Fedora distributions.
-
-        This can be removed when the fix is applied upstream.
-
-        Nova: https://bugs.launchpad.net/nova/+bug/884915
-        Upstream: https://bitbucket.org/which_linden/eventlet/issue/89
-        """
-
-        # Install "patch" program if it's not there
-        if not self.check_pkg('patch'):
-            self.yum_install('patch')
-
-        # Apply the eventlet patch
-        self.apply_patch(os.path.join(VENV, 'lib', PY_VERSION, 'site-packages',
-                                      'eventlet/green/subprocess.py'),
-                         'contrib/redhat-eventlet.patch')
-
-
-class CentOS(Fedora):
+class CentOS(install_venv.Fedora):
     """This covers CentOS."""
 
     def post_process(self):
@@ -149,73 +37,6 @@
             self.yum.install('openssl-devel', check_exit_code=False)
 
 
-def get_distro():
-    if os.path.exists('/etc/redhat-release'):
-        with open('/etc/redhat-release') as rh_release:
-            if 'CentOS' in rh_release.read():
-                return CentOS()
-        return Fedora()
-
-    if os.path.exists('/etc/fedora-release'):
-        return Fedora()
-
-    return Distro()
-
-
-def check_dependencies():
-    get_distro().install_virtualenv()
-
-
-def create_virtualenv(venv=VENV, no_site_packages=True):
-    """Creates the virtual environment and installs PIP.
-
-    Creates the virtual environment and installs PIP only into the
-    virtual environment.
-    """
-    print 'Creating venv...',
-    if no_site_packages:
-        run_command(['virtualenv', '-q', '--no-site-packages', VENV])
-    else:
-        run_command(['virtualenv', '-q', VENV])
-    print 'done.'
-    print 'Installing pip in virtualenv...',
-    if not run_command(['tools/with_venv.sh', 'easy_install',
-                        'pip>1.0']).strip():
-        die("Failed to install pip.")
-    print 'done.'
-
-
-def pip_install(*args):
-    run_command(['tools/with_venv.sh',
-                 'pip', 'install', '--upgrade'] + list(args),
-                redirect_output=False)
-
-
-def install_dependencies(venv=VENV):
-    print 'Installing dependencies with pip (this can take a while)...'
-
-    # First things first, make sure our venv has the latest pip and distribute.
-    # NOTE: we keep pip at version 1.1 since the most recent version causes
-    # the .venv creation to fail. See:
-    # https://bugs.launchpad.net/nova/+bug/1047120
-    pip_install('pip==1.1')
-    pip_install('distribute')
-
-    # Install greenlet by hand - just listing it in the requires file does not
-    # get it in stalled in the right order
-    pip_install('greenlet')
-
-    pip_install('-r', PIP_REQUIRES)
-    pip_install('-r', TEST_REQUIRES)
-
-    # Install tempest into the virtual_env. No more path munging!
-    run_command([os.path.join(venv, 'bin/python'), 'setup.py', 'develop'])
-
-
-def post_process():
-    get_distro().post_process()
-
-
 def print_help():
     help = """
     Tempest development environment setup is complete.
@@ -238,23 +59,25 @@
     print help
 
 
-def parse_args():
-    """Parses command-line arguments."""
-    parser = optparse.OptionParser()
-    parser.add_option("-n", "--no-site-packages", dest="no_site_packages",
-                      default=False, action="store_true",
-                      help="Do not inherit packages from global Python"
-                           " install")
-    return parser.parse_args()
-
-
 def main(argv):
-    (options, args) = parse_args()
-    check_python_version()
-    check_dependencies()
-    create_virtualenv(no_site_packages=options.no_site_packages)
-    install_dependencies()
-    post_process()
+    root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+    venv = os.path.join(root, '.venv')
+    pip_requires = os.path.join(root, 'tools', 'pip-requires')
+    test_requires = os.path.join(root, 'tools', 'test-requires')
+    py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
+    project = 'Tempest'
+    install = install_venv.InstallVenv(root, venv, pip_requires, test_requires,
+                                       py_version, project)
+    if os.path.exists('/etc/redhat-release'):
+        with open('/etc/redhat-release') as rh_release:
+            if 'CentOS' in rh_release.read():
+                install_venv.Fedora = CentOS
+    options = install.parse_args(argv)
+    install.check_python_version()
+    install.check_dependencies()
+    install.create_virtualenv(no_site_packages=options.no_site_packages)
+    install.install_dependencies()
+    install.post_process()
     print_help()
 
 if __name__ == '__main__':
diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py
new file mode 100644
index 0000000..3dbcafe
--- /dev/null
+++ b/tools/install_venv_common.py
@@ -0,0 +1,225 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack, LLC
+# Copyright 2013 IBM Corp.
+#
+#    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.
+
+"""Provides methods needed by installation script for OpenStack development
+virtual environments.
+
+Synced in from openstack-common
+"""
+
+import os
+import subprocess
+import sys
+
+from tempest.openstack.common import cfg
+
+
+class InstallVenv(object):
+
+    def __init__(self, root, venv, pip_requires, test_requires, py_version,
+                 project):
+        self.root = root
+        self.venv = venv
+        self.pip_requires = pip_requires
+        self.test_requires = test_requires
+        self.py_version = py_version
+        self.project = project
+
+    def die(self, message, *args):
+        print >> sys.stderr, message % args
+        sys.exit(1)
+
+    def check_python_version(self):
+        if sys.version_info < (2, 6):
+            self.die("Need Python Version >= 2.6")
+
+    def run_command_with_code(self, cmd, redirect_output=True,
+                              check_exit_code=True):
+        """Runs a command in an out-of-process shell.
+
+        Returns the output of that command. Working directory is ROOT.
+        """
+        if redirect_output:
+            stdout = subprocess.PIPE
+        else:
+            stdout = None
+
+        proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
+        output = proc.communicate()[0]
+        if check_exit_code and proc.returncode != 0:
+            self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
+        return (output, proc.returncode)
+
+    def run_command(self, cmd, redirect_output=True, check_exit_code=True):
+        return self.run_command_with_code(cmd, redirect_output,
+                                          check_exit_code)[0]
+
+    def get_distro(self):
+        if (os.path.exists('/etc/fedora-release') or
+                os.path.exists('/etc/redhat-release')):
+            return Fedora(self.root, self.venv, self.pip_requires,
+                          self.test_requires, self.py_version, self.project)
+        else:
+            return Distro(self.root, self.venv, self.pip_requires,
+                          self.test_requires, self.py_version, self.project)
+
+    def check_dependencies(self):
+        self.get_distro().install_virtualenv()
+
+    def create_virtualenv(self, no_site_packages=True):
+        """Creates the virtual environment and installs PIP.
+
+        Creates the virtual environment and installs PIP only into the
+        virtual environment.
+        """
+        if not os.path.isdir(self.venv):
+            print 'Creating venv...',
+            if no_site_packages:
+                self.run_command(['virtualenv', '-q', '--no-site-packages',
+                                 self.venv])
+            else:
+                self.run_command(['virtualenv', '-q', self.venv])
+            print 'done.'
+            print 'Installing pip in virtualenv...',
+            if not self.run_command(['tools/with_venv.sh', 'easy_install',
+                                    'pip>1.0']).strip():
+                self.die("Failed to install pip.")
+            print 'done.'
+        else:
+            print "venv already exists..."
+            pass
+
+    def pip_install(self, *args):
+        self.run_command(['tools/with_venv.sh',
+                         'pip', 'install', '--upgrade'] + list(args),
+                         redirect_output=False)
+
+    def install_dependencies(self):
+        print 'Installing dependencies with pip (this can take a while)...'
+
+        # First things first, make sure our venv has the latest pip and
+        # distribute.
+        # NOTE: we keep pip at version 1.1 since the most recent version causes
+        # the .venv creation to fail. See:
+        # https://bugs.launchpad.net/nova/+bug/1047120
+        self.pip_install('pip==1.1')
+        self.pip_install('distribute')
+
+        # Install greenlet by hand - just listing it in the requires file does
+        # not
+        # get it installed in the right order
+        self.pip_install('greenlet')
+
+        self.pip_install('-r', self.pip_requires)
+        self.pip_install('-r', self.test_requires)
+
+    def post_process(self):
+        self.get_distro().post_process()
+
+    def parse_args(self, argv):
+        """Parses command-line arguments."""
+        cli_opts = [
+            cfg.BoolOpt('no-site-packages',
+                        default=False,
+                        short='n',
+                        help="Do not inherit packages from global Python"
+                             "install"),
+        ]
+        CLI = cfg.ConfigOpts()
+        CLI.register_cli_opts(cli_opts)
+        CLI(argv[1:])
+        return CLI
+
+
+class Distro(InstallVenv):
+
+    def check_cmd(self, cmd):
+        return bool(self.run_command(['which', cmd],
+                    check_exit_code=False).strip())
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if self.check_cmd('easy_install'):
+            print 'Installing virtualenv via easy_install...',
+            if self.run_command(['easy_install', 'virtualenv']):
+                print 'Succeeded'
+                return
+            else:
+                print 'Failed'
+
+        self.die('ERROR: virtualenv not found.\n\n%s development'
+                 ' requires virtualenv, please install it using your'
+                 ' favorite package management tool' % self.project)
+
+    def post_process(self):
+        """Any distribution-specific post-processing gets done here.
+
+        In particular, this is useful for applying patches to code inside
+        the venv.
+        """
+        pass
+
+
+class Fedora(Distro):
+    """This covers all Fedora-based distributions.
+
+    Includes: Fedora, RHEL, CentOS, Scientific Linux
+    """
+
+    def check_pkg(self, pkg):
+        return self.run_command_with_code(['rpm', '-q', pkg],
+                                          check_exit_code=False)[1] == 0
+
+    def yum_install(self, pkg, **kwargs):
+        print "Attempting to install '%s' via yum" % pkg
+        self.run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
+
+    def apply_patch(self, originalfile, patchfile):
+        self.run_command(['patch', originalfile, patchfile])
+
+    def install_virtualenv(self):
+        if self.check_cmd('virtualenv'):
+            return
+
+        if not self.check_pkg('python-virtualenv'):
+            self.yum_install('python-virtualenv', check_exit_code=False)
+
+        super(Fedora, self).install_virtualenv()
+
+    def post_process(self):
+        """Workaround for a bug in eventlet.
+
+        This currently affects RHEL6.1, but the fix can safely be
+        applied to all RHEL and Fedora distributions.
+
+        This can be removed when the fix is applied upstream.
+
+        Nova: https://bugs.launchpad.net/nova/+bug/884915
+        Upstream: https://bitbucket.org/which_linden/eventlet/issue/89
+        """
+
+        # Install "patch" program if it's not there
+        if not self.check_pkg('patch'):
+            self.yum_install('patch')
+
+        # Apply the eventlet patch
+        self.apply_patch(os.path.join(self.venv, 'lib', self.py_version,
+                                      'site-packages',
+                                      'eventlet/green/subprocess.py'),
+                         'contrib/redhat-eventlet.patch')
diff --git a/tools/pip-requires b/tools/pip-requires
index dcc859f..504ca24 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -11,3 +11,5 @@
 python-novaclient>=2.10.0
 python-quantumclient>=2.1
 testresources
+unittest2
+keyring