Merge "Update gitignore because of oslo setup.py"
diff --git a/run_tests.sh b/run_tests.sh
index e350c13..6b7ebec 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -10,6 +10,7 @@
   echo "  -f, --force              Force a clean re-build of the virtual environment. Useful when dependencies have been added."
   echo "  -s, --smoke              Only run smoke tests"
   echo "  -w, --whitebox           Only run whitebox tests"
+  echo "  -c, --nova-coverage      Enable Nova coverage collection"
   echo "  -p, --pep8               Just run pep8"
   echo "  -h, --help               Print this usage message"
   echo "  -d, --debug              Debug this script -- set -o xtrace"
@@ -25,6 +26,7 @@
     -s|--no-site-packages) no_site_packages=1;;
     -f|--force) force=1;;
     -d|--debug) set -o xtrace;;
+    -c|--nova-coverage) let nova_coverage=1;;
     -p|--pep8) let just_pep8=1;;
     -s|--smoke) noseargs="$noseargs --attr=type=smoke";;
     -w|--whitebox) noseargs="$noseargs --attr=type=whitebox";;
@@ -42,7 +44,7 @@
 no_site_packages=0
 force=0
 wrapper=""
-
+nova_coverage=0
 
 export NOSE_WITH_OPENSTACK=1
 export NOSE_OPENSTACK_COLOR=1
@@ -83,6 +85,16 @@
     ${wrapper} python tools/hacking.py ${ignore} ${srcfiles}
 }
 
+function run_coverage_start {
+  echo "Starting nova-coverage"
+  ${wrapper} python tools/tempest_coverage.py -c start
+}
+
+function run_coverage_report {
+  echo "Generating nova-coverage report"
+  ${wrapper} python tools/tempest_coverage.py -c report
+}
+
 NOSETESTS="nosetests $noseargs"
 
 if [ $never_venv -eq 0 ]
@@ -116,7 +128,15 @@
     exit
 fi
 
-run_tests || exit
+if [ $nova_coverage -eq 1 ]; then
+    run_coverage_start
+fi
+
+run_tests
+
+if [ $nova_coverage -eq 1 ]; then
+    run_coverage_report
+fi
 
 if [ -z "$noseargs" ]; then
   run_pep8
diff --git a/tempest/tests/identity/admin/test_tenants.py b/tempest/tests/identity/admin/test_tenants.py
index 8fba7e3..578af4a 100644
--- a/tempest/tests/identity/admin/test_tenants.py
+++ b/tempest/tests/identity/admin/test_tenants.py
@@ -17,6 +17,7 @@
 
 import unittest2 as unittest
 
+from nose.plugins.attrib import attr
 from tempest.common.utils.data_utils import rand_name
 from tempest import exceptions
 from tempest.tests.identity import base
@@ -24,21 +25,6 @@
 
 class TenantsTestBase(object):
 
-    @staticmethod
-    def setUpClass(cls):
-        for _ in xrange(5):
-            resp, tenant = cls.client.create_tenant(rand_name('tenant-'))
-            cls.data.tenants.append(tenant)
-
-    def test_list_tenants(self):
-        # Return a list of all tenants
-        resp, body = self.client.list_tenants()
-        found = [tenant for tenant in body if tenant in self.data.tenants]
-        self.assertTrue(any(found), 'List did not return newly created '
-                        'tenants')
-        self.assertEqual(len(found), len(self.data.tenants))
-        self.assertTrue(resp['status'].startswith('2'))
-
     def test_list_tenants_by_unauthorized_user(self):
         # Non-admin user should not be able to list tenants
         self.assertRaises(exceptions.Unauthorized,
@@ -51,41 +37,50 @@
         self.assertRaises(exceptions.Unauthorized, self.client.list_tenants)
         self.client.clear_auth()
 
-    def test_tenant_delete(self):
+    def test_tenant_list_delete(self):
         # Create several tenants and delete them
         tenants = []
-        for _ in xrange(5):
-            resp, body = self.client.create_tenant(rand_name('tenant-new'))
-            tenants.append(body['id'])
-
+        for _ in xrange(3):
+            resp, tenant = self.client.create_tenant(rand_name('tenant-new'))
+            self.data.tenants.append(tenant)
+            tenants.append(tenant)
+        tenant_ids = map(lambda x: x['id'], tenants)
         resp, body = self.client.list_tenants()
-        found_1 = [tenant for tenant in body if tenant['id'] in tenants]
-        for tenant_id in tenants:
-            resp, body = self.client.delete_tenant(tenant_id)
+        self.assertTrue(resp['status'].startswith('2'))
+        found = [tenant for tenant in body if tenant['id'] in tenant_ids]
+        self.assertEqual(len(found), len(tenants), 'Tenants not created')
+
+        for tenant in tenants:
+            resp, body = self.client.delete_tenant(tenant['id'])
             self.assertTrue(resp['status'].startswith('2'))
+            self.data.tenants.remove(tenant)
 
         resp, body = self.client.list_tenants()
-        found_2 = [tenant for tenant in body if tenant['id'] in tenants]
-        self.assertTrue(any(found_1), 'Tenants not created')
-        self.assertFalse(any(found_2), 'Tenants failed to delete')
+        found = [tenant for tenant in body if tenant['id'] in tenant_ids]
+        self.assertFalse(any(found), 'Tenants failed to delete')
 
+    @attr(type='negative')
     def test_tenant_delete_by_unauthorized_user(self):
         # Non-admin user should not be able to delete a tenant
         tenant_name = rand_name('tenant-')
         resp, tenant = self.client.create_tenant(tenant_name)
+        self.data.tenants.append(tenant)
         self.assertRaises(exceptions.Unauthorized,
                           self.non_admin_client.delete_tenant, tenant['id'])
 
+    @attr(type='negative')
     def test_tenant_delete_request_without_token(self):
         # Request to delete a tenant without a valid token should fail
         tenant_name = rand_name('tenant-')
         resp, tenant = self.client.create_tenant(tenant_name)
+        self.data.tenants.append(tenant)
         token = self.client.get_auth()
         self.client.delete_token(token)
         self.assertRaises(exceptions.Unauthorized, self.client.delete_tenant,
                           tenant['id'])
         self.client.clear_auth()
 
+    @attr(type='negative')
     def test_delete_non_existent_tenant(self):
         # Attempt to delete a non existent tenant should fail
         self.assertRaises(exceptions.NotFound, self.client.delete_tenant,
@@ -97,6 +92,8 @@
         tenant_desc = rand_name('desc-')
         resp, body = self.client.create_tenant(tenant_name,
                                                description=tenant_desc)
+        tenant = body
+        self.data.tenants.append(tenant)
         st1 = resp['status']
         tenant_id = body['id']
         desc1 = body['description']
@@ -108,11 +105,14 @@
         self.assertEqual(desc2, tenant_desc, 'Description does not appear'
                          'to be set')
         self.client.delete_tenant(tenant_id)
+        self.data.tenants.remove(tenant)
 
     def test_tenant_create_enabled(self):
         # Create a tenant that is enabled
         tenant_name = rand_name('tenant-')
         resp, body = self.client.create_tenant(tenant_name, enabled=True)
+        tenant = body
+        self.data.tenants.append(tenant)
         tenant_id = body['id']
         st1 = resp['status']
         en1 = body['enabled']
@@ -122,11 +122,14 @@
         en2 = body['enabled']
         self.assertTrue(en2, 'Enable should be True in lookup')
         self.client.delete_tenant(tenant_id)
+        self.data.tenants.remove(tenant)
 
     def test_tenant_create_not_enabled(self):
         # Create a tenant that is not enabled
         tenant_name = rand_name('tenant-')
         resp, body = self.client.create_tenant(tenant_name, enabled=False)
+        tenant = body
+        self.data.tenants.append(tenant)
         tenant_id = body['id']
         st1 = resp['status']
         en1 = body['enabled']
@@ -138,11 +141,15 @@
         self.assertEqual('false', str(en2).lower(),
                          'Enable should be False in lookup')
         self.client.delete_tenant(tenant_id)
+        self.data.tenants.remove(tenant)
 
+    @attr(type='negative')
     def test_tenant_create_duplicate(self):
         # Tenant names should be unique
         tenant_name = rand_name('tenant-dup-')
         resp, body = self.client.create_tenant(tenant_name)
+        tenant = body
+        self.data.tenants.append(tenant)
         tenant1_id = body.get('id')
 
         try:
@@ -151,15 +158,17 @@
             self.fail('Should not be able to create a duplicate tenant name')
         except exceptions.Duplicate:
             pass
-        if tenant1_id:
-            self.client.delete_tenant(tenant1_id)
+        self.client.delete_tenant(tenant1_id)
+        self.data.tenants.remove(tenant)
 
+    @attr(type='negative')
     def test_create_tenant_by_unauthorized_user(self):
         # Non-admin user should not be authorized to create a tenant
         tenant_name = rand_name('tenant-')
         self.assertRaises(exceptions.Unauthorized,
                           self.non_admin_client.create_tenant, tenant_name)
 
+    @attr(type='negative')
     def test_create_tenant_request_without_token(self):
         # Create tenant request without a token should not be authorized
         tenant_name = rand_name('tenant-')
@@ -169,6 +178,7 @@
                           tenant_name)
         self.client.clear_auth()
 
+    @attr(type='negative')
     def test_create_tenant_with_empty_name(self):
         # Tenant name should not be empty
         self.assertRaises(exceptions.BadRequest, self.client.create_tenant,
@@ -184,6 +194,9 @@
         # Update name attribute of a tenant
         t_name1 = rand_name('tenant-')
         resp, body = self.client.create_tenant(t_name1)
+        tenant = body
+        self.data.tenants.append(tenant)
+
         t_id = body['id']
         resp1_name = body['name']
 
@@ -202,12 +215,16 @@
         self.assertEqual(resp2_name, resp3_name)
 
         self.client.delete_tenant(t_id)
+        self.data.tenants.remove(tenant)
 
     def test_tenant_update_desc(self):
         # Update description attribute of a tenant
         t_name = rand_name('tenant-')
         t_desc = rand_name('desc-')
         resp, body = self.client.create_tenant(t_name, description=t_desc)
+        tenant = body
+        self.data.tenants.append(tenant)
+
         t_id = body['id']
         resp1_desc = body['description']
 
@@ -226,12 +243,16 @@
         self.assertEqual(resp2_desc, resp3_desc)
 
         self.client.delete_tenant(t_id)
+        self.data.tenants.remove(tenant)
 
     def test_tenant_update_enable(self):
         # Update the enabled attribute of a tenant
         t_name = rand_name('tenant-')
         t_en = False
         resp, body = self.client.create_tenant(t_name, enabled=t_en)
+        tenant = body
+        self.data.tenants.append(tenant)
+
         t_id = body['id']
         resp1_en = body['enabled']
 
@@ -250,6 +271,7 @@
         self.assertEqual(resp2_en, resp3_en)
 
         self.client.delete_tenant(t_id)
+        self.data.tenants.remove(tenant)
 
 
 class TenantsTestJSON(base.BaseIdentityAdminTestJSON,
@@ -258,7 +280,6 @@
     @classmethod
     def setUpClass(cls):
         super(TenantsTestJSON, cls).setUpClass()
-        TenantsTestBase.setUpClass(cls)
 
 
 class TenantsTestXML(base.BaseIdentityAdminTestXML, TenantsTestBase):
@@ -266,4 +287,3 @@
     @classmethod
     def setUpClass(cls):
         super(TenantsTestXML, cls).setUpClass()
-        TenantsTestBase.setUpClass(cls)
diff --git a/tempest/tests/volume/admin/test_volume_types.py b/tempest/tests/volume/admin/test_volume_types.py
index 8ebb78f..65c975a 100644
--- a/tempest/tests/volume/admin/test_volume_types.py
+++ b/tempest/tests/volume/admin/test_volume_types.py
@@ -126,7 +126,8 @@
         try:
             body = {}
             name = rand_name("volume-type-")
-            extra_specs = {"Spec1": "Val1", "Spec2": "Val2"}
+            extra_specs = {"storage_protocol": "iSCSI",
+                           "vendor_name": "Open Source"}
             resp, body = self.client.\
             create_volume_type(name, extra_specs=extra_specs)
             self.assertEqual(200, resp.status)
diff --git a/tools/tempest_coverage.py b/tools/tempest_coverage.py
new file mode 100755
index 0000000..73dcfbc
--- /dev/null
+++ b/tools/tempest_coverage.py
@@ -0,0 +1,194 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 IBM
+#
+#    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
+
+import json
+import os
+import re
+import shutil
+import sys
+
+from tempest.common.rest_client import RestClient
+from tempest import config
+from tempest.openstack.common import cfg
+from tempest.tests.compute import base
+
+CONF = config.TempestConfig()
+
+
+class CoverageClientJSON(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(CoverageClientJSON, self).__init__(config, username, password,
+                                                 auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+
+    def start_coverage(self):
+        post_body = {
+            'start': {},
+        }
+        post_body = json.dumps(post_body)
+        return self.post('os-coverage/action', post_body, self.headers)
+
+    def start_coverage_combine(self):
+        post_body = {
+            'start': {
+                'combine': True,
+            },
+        }
+        post_body = json.dumps(post_body)
+        return self.post('os-coverage/action', post_body, self.headers)
+
+    def stop_coverage(self):
+        post_body = {
+            'stop': {},
+        }
+        post_body = json.dumps(post_body)
+        resp, body = self.post('os-coverage/action', post_body, self.headers)
+        body = json.loads(body)
+        return resp, body
+
+    def report_coverage_xml(self, file=None):
+        post_body = {
+            'report': {
+                'file': 'coverage.report',
+                'xml': True,
+            },
+        }
+        if file:
+            post_body['report']['file'] = file
+        post_body = json.dumps(post_body)
+        resp, body = self.post('os-coverage/action', post_body, self.headers)
+        body = json.loads(body)
+        return resp, body
+
+    def report_coverage(self, file=None):
+        post_body = {
+            'report': {
+                'file': 'coverage.report',
+            },
+        }
+        if file:
+            post_body['report']['file'] = file
+        post_body = json.dumps(post_body)
+        resp, body = self.post('os-coverage/action', post_body, self.headers)
+        body = json.loads(body)
+        return resp, body
+
+    def report_coverage_html(self, file=None):
+        post_body = {
+            'report': {
+                'file': 'coverage.report',
+                'html': True,
+            },
+        }
+        if file:
+            post_body['report']['file'] = file
+        post_body = json.dumps(post_body)
+        resp, body = self.post('os-coverage/action', post_body, self.headers)
+        body = json.loads(body)
+        return resp, body
+
+
+def parse_opts(argv):
+    cli_opts = [
+        cfg.StrOpt('command',
+                   short='c',
+                   default='',
+                   help="This required argument is used to specify the "
+                        "coverage command to run. Only 'start', "
+                        "'stop', or 'report' are valid fields."),
+        cfg.StrOpt('filename',
+                   default='tempest-coverage',
+                   help="Specify a filename to be used for generated report "
+                        "files"),
+        cfg.BoolOpt('xml',
+                    default=False,
+                    help='Generate XML reports instead of text'),
+        cfg.BoolOpt('html',
+                    default=False,
+                    help='Generate HTML reports instead of text'),
+        cfg.BoolOpt('combine',
+                    default=False,
+                    help='Generate a single report for all services'),
+        cfg.StrOpt('output',
+                   short='o',
+                   default=None,
+                   help='Optional directory to copy generated coverage data or'
+                        ' reports into. This directory must not already exist '
+                        'it will be created')
+    ]
+    CLI = cfg.ConfigOpts()
+    CLI.register_cli_opts(cli_opts)
+    CLI(argv[1:])
+    return CLI
+
+
+def main(argv):
+    CLI = parse_opts(argv)
+    client_args = (CONF, CONF.compute_admin.username,
+                   CONF.compute_admin.password, CONF.identity.auth_url,
+                   CONF.compute_admin.tenant_name)
+    coverage_client = CoverageClientJSON(*client_args)
+
+    if CLI.command == 'start':
+        if CLI.combine:
+            coverage_client.start_coverage_combine()
+        else:
+            coverage_client.start_coverage()
+
+    elif CLI.command == 'stop':
+        resp, body = coverage_client.stop_coverage()
+        if not resp['status'] == '200':
+            print 'coverage stop failed with: %s:' % (resp['status'] + ': '
+                                                      + body)
+            exit(int(resp['status']))
+        path = body['path']
+        if CLI.output:
+            shutil.copytree(path, CLI.output)
+        else:
+            print "Data files located at: %s" % path
+
+    elif CLI.command == 'report':
+        if CLI.xml:
+            resp, body = coverage_client.report_coverage_xml(file=CLI.filename)
+        elif CLI.html:
+            resp, body = coverage_client.report_coverage_html(
+                                                            file=CLI.filename)
+        else:
+            resp, body = coverage_client.report_coverage(file=CLI.filename)
+        if not resp['status'] == '200':
+            print 'coverage report failed with: %s:' % (resp['status'] + ': '
+                                                        + body)
+            exit(int(resp['status']))
+        path = body['path']
+        if CLI.output:
+            if CLI.html:
+                shutil.copytree(path, CLI.output)
+            else:
+                path = os.path.dirname(path)
+                shutil.copytree(path, CLI.output)
+        else:
+            if not CLI.html:
+                path = os.path.dirname(path)
+            print 'Report files located at: %s' % path
+
+    else:
+        print 'Invalid command'
+        exit(1)
+
+
+if __name__ == "__main__":
+    main(sys.argv)
diff --git a/tox.ini b/tox.ini
index da1672b..33ca1c4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,6 +13,11 @@
        -r{toxinidir}/tools/test-requires
 commands = nosetests {posargs}
 
+[testenv:coverage]
+commands = python tools/tempest_coverage.py -c start --combine
+           nosetests {posargs}
+           python tools/tempest_coverage.py -c report --html
+
 [testenv:pep8]
 deps = pep8==1.3.3
 commands = python tools/hacking.py --ignore=E122,E125,E126 --repeat --show-source --exclude=.venv,.tox,dist,doc,openstack,*egg .