Merge "Refactor duplicate isolated creds code"
diff --git a/doc/source/conf.py b/doc/source/conf.py
index cf838c0..e5444ae 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -51,17 +51,6 @@
 project = u'Tempest'
 copyright = u'2013, OpenStack QA Team'
 
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-import pbr.version
-version_info = pbr.version.VersionInfo('tempest')
-version = version_info.version_string()
-# The full version, including alpha/beta/rc tags.
-release = version_info.release_string()
-
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 #language = None
diff --git a/etc/whitelist.yaml b/etc/whitelist.yaml
index 6762f9f..e6d28f5 100644
--- a/etc/whitelist.yaml
+++ b/etc/whitelist.yaml
@@ -21,6 +21,16 @@
       message: "Instance failed to spawn"
     - module: "nova.compute.manager"
       message: "Error: Unexpected error while running command"
+    - module: "nova.virt.libvirt.driver"
+      message: "Error from libvirt during destroy"
+    - module: "nova.virt.libvirt.vif"
+      message: "Failed while unplugging vif"
+    - module: "nova.openstack.common.loopingcal"
+      message: "in fixed duration looping call"
+    - module: "nova.virt.libvirt.driver"
+      message: "Getting disk size of instance"
+    - module: "nova.virt.libvirt.driver"
+      message: "No such file or directory: '/opt/stack/data/nova/instances"
 
 g-api:
     - module: "glance.store.sheepdog"
@@ -34,6 +44,12 @@
       message: "Requested operation is not valid: domain is not running"
     - module: "ceilometer.compute.pollsters.disk"
       message: "Domain not found: no domain with matching uuid"
+    - module: "ceilometer.compute.pollsters.net"
+      message: "No module named libvirt"
+
+ceilometer-alarm-evaluator:
+    - module: "ceilometer.alarm.service"
+      message: "alarm evaluation cycle failed"
 
 h-api:
     - module: "root"
@@ -63,6 +79,8 @@
       message: "ServersTest"
     - module: "nova.compute.api"
       message: "\\{u'kernel_id'.*u'ramdisk_id':"
+    - module: "nova.api.openstack.wsgi"
+      message: "takes exactly 4 arguments"
 
 n-cond:
     - module: "nova.notifications"
@@ -75,6 +93,16 @@
 c-api:
     - module: "cinder.api.middleware.fault"
       message: "Caught error: Volume .* could not be found"
+    - module: "cinder.api.middleware.fault"
+      message: "Caught error: Snapshot .* could not be found"
+
+c-vol:
+    - module: "cinder.brick.iscsi.iscsi"
+      message: "Failed to create iscsi target for volume id"
+    - module: "cinder.brick.local_dev.lvm"
+      message: "/dev/dm-1: stat failed: No such file or directory"
+    - module: "cinder.brick.local_dev.lvm"
+      message: "Can't remove open logical volume"
 
 q-dhpc:
     - module: "neutron.common.legacy"
@@ -89,6 +117,8 @@
       message: ".*"
     - module: "ceilometer.collector.dispatcher.database"
       message: "duplicate key value violates unique constraint"
+    - module: "ceilometer.collector.dispatcher.database"
+      message: "Failed to record metering data: QueuePool limit"
 
 q-agt:
     - module: "neutron.agent.linux.ovs_lib"
@@ -108,6 +138,9 @@
     - module: "neutron.agent.l3_agent"
       message: "Failed synchronizing routers"
 
+q-vpn:
+    - module: "neutron.common.legacy"
+      message: "Skipping unknown group key: firewall_driver"
 
 q-lbaas:
     - module: "neutron.common.legacy"
@@ -123,9 +156,7 @@
     - module: "neutron.openstack.common.rpc.amqp"
       message: "Exception during message handling"
     - module: "neutron.openstack.common.rpc.common"
-      message: "Network .* could not be found"
-    - module: "neutron.openstack.common.rpc.common"
-      message: "Pool .* could not be found"
+      message: "(Network|Pool|Subnet|Agent|Port) .* could not be found"
     - module: "neutron.api.v2.resource"
-      message: "show failed"
+      message: ".* failed"
 
diff --git a/requirements.txt b/requirements.txt
index 4dea533..4f6a1d3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,17 +5,17 @@
 jsonschema>=1.3.0,!=1.4.0
 testtools>=0.9.32
 lxml>=2.3
-boto>=2.4.0
+boto>=2.4.0,!=2.13.0
 paramiko>=1.8.0
 netaddr
 python-glanceclient>=0.9.0
-python-keystoneclient>=0.3.0
-python-novaclient>=2.12.0
-python-neutronclient>=2.2.3,<3
-python-cinderclient>=1.0.4
+python-keystoneclient>=0.4.1
+python-novaclient>=2.15.0
+python-neutronclient>=2.3.0,<3
+python-cinderclient>=1.0.6
 python-heatclient>=0.2.3
 testresources>=0.2.4
-keyring>=1.6.1
+keyring>=1.6.1,<2.0
 testrepository>=0.0.17
 oslo.config>=1.2.0
 eventlet>=0.13.0
diff --git a/setup.cfg b/setup.cfg
index a4cf118..23a97ff 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = tempest
-version = 2013.2
+version = 2014.1
 summary = OpenStack Integration Testing
 description-file =
     README.rst
@@ -17,20 +17,7 @@
     Programming Language :: Python :: 2
     Programming Language :: Python :: 2.7
 
-[global]
-setup-hooks =
-    pbr.hooks.setup_hook
-
 [build_sphinx]
 all_files = 1
 build-dir = doc/build
 source-dir = doc/source
-
-[nosetests]
-# NOTE(jkoelker) To run the test suite under nose install the following
-#                coverage http://pypi.python.org/pypi/coverage
-#                openstack-nose https://github.com/openstack-dev/openstack-nose
-verbosity=2
-
-[pbr]
-autodoc_tree_index_modules=true
diff --git a/setup.py b/setup.py
index 59a0090..70c2b3f 100755
--- a/setup.py
+++ b/setup.py
@@ -14,8 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
 import setuptools
 
 setuptools.setup(
-    setup_requires=['d2to1', 'pbr'],
-    d2to1=True)
+    setup_requires=['pbr'],
+    pbr=True)
diff --git a/tempest/api/compute/admin/test_aggregates.py b/tempest/api/compute/admin/test_aggregates.py
index e8acec5..4ff6b07 100644
--- a/tempest/api/compute/admin/test_aggregates.py
+++ b/tempest/api/compute/admin/test_aggregates.py
@@ -85,7 +85,7 @@
                           aggregates))
 
     @attr(type='gate')
-    def test_aggregate_create_get_details(self):
+    def test_aggregate_create_update_metadata_get_details(self):
         # Create an aggregate and ensure its details are returned.
         aggregate_name = rand_name(self.aggregate_name_prefix)
         resp, aggregate = self.client.create_aggregate(aggregate_name)
@@ -96,6 +96,18 @@
         self.assertEqual(aggregate['name'], body['name'])
         self.assertEqual(aggregate['availability_zone'],
                          body['availability_zone'])
+        self.assertEqual({}, body["metadata"])
+
+        # set the metadata of the aggregate
+        meta = {"key": "value"}
+        resp, body = self.client.set_metadata(aggregate['id'], meta)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(meta, body["metadata"])
+
+        # verify the metadata has been set
+        resp, body = self.client.get_aggregate(aggregate['id'])
+        self.assertEqual(200, resp.status)
+        self.assertEqual(meta, body["metadata"])
 
     @attr(type='gate')
     def test_aggregate_create_update_with_az(self):
diff --git a/tempest/api/compute/admin/test_hosts.py b/tempest/api/compute/admin/test_hosts.py
index aa769ba..8e451a0 100644
--- a/tempest/api/compute/admin/test_hosts.py
+++ b/tempest/api/compute/admin/test_hosts.py
@@ -16,8 +16,6 @@
 
 from tempest.api.compute import base
 from tempest.common import tempest_fixtures as fixtures
-from tempest.common.utils.data_utils import rand_name
-from tempest import exceptions
 from tempest.test import attr
 
 
@@ -33,7 +31,13 @@
     def setUpClass(cls):
         super(HostsAdminTestJSON, cls).setUpClass()
         cls.client = cls.os_adm.hosts_client
-        cls.non_admin_client = cls.os.hosts_client
+
+    def _get_host_name(self):
+        resp, hosts = self.client.list_hosts()
+        self.assertEqual(200, resp.status)
+        self.assertTrue(len(hosts) >= 1)
+        hostname = hosts[0]['host_name']
+        return hostname
 
     @attr(type='gate')
     def test_list_hosts(self):
@@ -53,14 +57,7 @@
         self.assertTrue(len(hosts) >= 1)
         self.assertIn(host, hosts)
 
-    @attr(type='negative')
-    def test_list_hosts_with_non_existent_zone(self):
-        params = {'zone': 'xxx'}
-        resp, hosts = self.client.list_hosts(params)
-        self.assertEqual(0, len(hosts))
-        self.assertEqual(200, resp.status)
-
-    @attr(type='negative')
+    @attr(type='gate')
     def test_list_hosts_with_a_blank_zone(self):
         # If send the request with a blank zone, the request will be successful
         # and it will return all the hosts list
@@ -69,17 +66,18 @@
         self.assertNotEqual(0, len(hosts))
         self.assertEqual(200, resp.status)
 
-    @attr(type=['negative', 'gate'])
-    def test_list_hosts_with_non_admin_user(self):
-        self.assertRaises(exceptions.Unauthorized,
-                          self.non_admin_client.list_hosts)
+    @attr(type='gate')
+    def test_list_hosts_with_nonexistent_zone(self):
+        # If send the request with a nonexistent zone, the request will be
+        # successful and no hosts will be retured
+        params = {'zone': 'xxx'}
+        resp, hosts = self.client.list_hosts(params)
+        self.assertEqual(0, len(hosts))
+        self.assertEqual(200, resp.status)
 
     @attr(type='gate')
     def test_show_host_detail(self):
-        resp, hosts = self.client.list_hosts()
-        self.assertEqual(200, resp.status)
-        self.assertTrue(len(hosts) >= 1)
-        hostname = hosts[0]['host_name']
+        hostname = self._get_host_name()
 
         resp, resources = self.client.show_host_detail(hostname)
         self.assertEqual(200, resp.status)
@@ -92,12 +90,6 @@
         self.assertIsNotNone(host_resource['project'])
         self.assertEqual(hostname, host_resource['host'])
 
-    @attr(type='negative')
-    def test_show_host_detail_with_nonexist_hostname(self):
-        hostname = rand_name('rand_hostname')
-        self.assertRaises(exceptions.NotFound,
-                          self.client.show_host_detail, hostname)
-
 
 class HostsAdminTestXML(HostsAdminTestJSON):
     _interface = 'xml'
diff --git a/tempest/api/compute/admin/test_hosts_negative.py b/tempest/api/compute/admin/test_hosts_negative.py
new file mode 100644
index 0000000..9d4c62b
--- /dev/null
+++ b/tempest/api/compute/admin/test_hosts_negative.py
@@ -0,0 +1,174 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Huawei Technologies Co.,LTD.
+#
+#    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.
+
+from tempest.api.compute import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class HostsAdminNegativeTestJSON(base.BaseComputeAdminTest):
+
+    """
+    Tests hosts API using admin privileges.
+    """
+
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(HostsAdminNegativeTestJSON, cls).setUpClass()
+        cls.client = cls.os_adm.hosts_client
+        cls.non_admin_client = cls.os.hosts_client
+
+    def _get_host_name(self):
+        resp, hosts = self.client.list_hosts()
+        self.assertEqual(200, resp.status)
+        self.assertTrue(len(hosts) >= 1)
+        hostname = hosts[0]['host_name']
+        return hostname
+
+    @attr(type=['negative', 'gate'])
+    def test_list_hosts_with_non_admin_user(self):
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.list_hosts)
+
+    @attr(type=['negative', 'gate'])
+    def test_show_host_detail_with_nonexistent_hostname(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+        self.assertRaises(exceptions.NotFound,
+                          self.client.show_host_detail, nonexitent_hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_show_host_detail_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.show_host_detail,
+                          hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_update_host_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.update_host,
+                          hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_update_host_with_extra_param(self):
+        # only 'status' and 'maintenance_mode' are the valid params.
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_host,
+                          hostname,
+                          status='enable',
+                          maintenance_mode='enable',
+                          param='XXX')
+
+    @attr(type=['negative', 'gate'])
+    def test_update_host_with_invalid_status(self):
+        # 'status' can only be 'enable' or 'disable'
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_host,
+                          hostname,
+                          status='invalid',
+                          maintenance_mode='enable')
+
+    @attr(type=['negative', 'gate'])
+    def test_update_host_with_invalid_maintenance_mode(self):
+        # 'maintenance_mode' can only be 'enable' or 'disable'
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_host,
+                          hostname,
+                          status='enable',
+                          maintenance_mode='invalid')
+
+    @attr(type=['negative', 'gate'])
+    def test_update_host_without_param(self):
+        # 'status' or 'maintenance_mode' needed for host update
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_host,
+                          hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_update_nonexistent_host(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+        self.assertRaises(exceptions.NotFound,
+                          self.client.update_host,
+                          nonexitent_hostname,
+                          status='enable',
+                          maintenance_mode='enable')
+
+    @attr(type=['negative', 'gate'])
+    def test_startup_nonexistent_host(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+        self.assertRaises(exceptions.NotFound,
+                          self.client.startup_host,
+                          nonexitent_hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_startup_host_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.startup_host,
+                          hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_shutdown_nonexistent_host(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+        self.assertRaises(exceptions.NotFound,
+                          self.client.shutdown_host,
+                          nonexitent_hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_shutdown_host_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.shutdown_host,
+                          hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_reboot_nonexistent_host(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+        self.assertRaises(exceptions.NotFound,
+                          self.client.reboot_host,
+                          nonexitent_hostname)
+
+    @attr(type=['negative', 'gate'])
+    def test_reboot_host_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.reboot_host,
+                          hostname)
+
+
+class HostsAdminNegativeTestXML(HostsAdminNegativeTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/admin/test_hypervisor_negative.py b/tempest/api/compute/admin/test_hypervisor_negative.py
index 69b8d9c..c6455b5 100644
--- a/tempest/api/compute/admin/test_hypervisor_negative.py
+++ b/tempest/api/compute/admin/test_hypervisor_negative.py
@@ -23,7 +23,7 @@
 from tempest.test import attr
 
 
-class HypervisorAdminNegativeTestJSON(base.BaseComputeAdminTest):
+class HypervisorAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
 
     """
     Tests Hypervisors API that require admin privileges
diff --git a/tempest/api/image/v2/test_images_tags.py b/tempest/api/image/v2/test_images_tags.py
new file mode 100644
index 0000000..7e3bde4
--- /dev/null
+++ b/tempest/api/image/v2/test_images_tags.py
@@ -0,0 +1,45 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.
+
+from tempest.api.image import base
+from tempest.common.utils import data_utils
+from tempest.test import attr
+
+
+class ImagesTagsTest(base.BaseV2ImageTest):
+
+    @attr(type='gate')
+    def test_update_delete_tags_for_image(self):
+        resp, body = self.create_image(container_format='bare',
+                                       disk_format='raw',
+                                       visibility='public')
+        image_id = body['id']
+        tag = data_utils.rand_name('tag-')
+        self.addCleanup(self.client.delete_image, image_id)
+
+        # Creating image tag and verify it.
+        resp, body = self.client.add_image_tag(image_id, tag)
+        self.assertEqual(resp.status, 204)
+        resp, body = self.client.get_image_metadata(image_id)
+        self.assertEqual(resp.status, 200)
+        self.assertIn(tag, body['tags'])
+
+        # Deleting image tag and verify it.
+        resp = self.client.delete_image_tag(image_id, tag)
+        self.assertEqual(resp.status, 204)
+        resp, body = self.client.get_image_metadata(image_id)
+        self.assertEqual(resp.status, 200)
+        self.assertNotIn(tag, body['tags'])
diff --git a/tempest/api/image/v2/test_images_tags_negative.py b/tempest/api/image/v2/test_images_tags_negative.py
new file mode 100644
index 0000000..e0d84de
--- /dev/null
+++ b/tempest/api/image/v2/test_images_tags_negative.py
@@ -0,0 +1,46 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.
+
+import uuid
+
+from tempest.api.image import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class ImagesTagsNegativeTest(base.BaseV2ImageTest):
+
+    @attr(type=['negative', 'gate'])
+    def test_update_tags_for_non_existing_image(self):
+        # Update tag with non existing image.
+        tag = data_utils.rand_name('tag-')
+        non_exist_image = str(uuid.uuid4())
+        self.assertRaises(exceptions.NotFound, self.client.add_image_tag,
+                          non_exist_image, tag)
+
+    @attr(type=['negative', 'gate'])
+    def test_delete_non_existing_tag(self):
+        # Delete non existing tag.
+        resp, body = self.create_image(container_format='bare',
+                                       disk_format='raw',
+                                       is_public=True,
+                                       )
+        image_id = body['id']
+        tag = data_utils.rand_name('non-exist-tag-')
+        self.addCleanup(self.client.delete_image, image_id)
+        self.assertRaises(exceptions.NotFound, self.client.delete_image_tag,
+                          image_id, tag)
diff --git a/tempest/api/network/base.py b/tempest/api/network/base.py
index cdb8e17..159c4f5 100644
--- a/tempest/api/network/base.py
+++ b/tempest/api/network/base.py
@@ -18,7 +18,6 @@
 import netaddr
 
 from tempest import clients
-from tempest.common import isolated_creds
 from tempest.common.utils.data_utils import rand_name
 from tempest import exceptions
 from tempest.openstack.common import log as logging
@@ -52,19 +51,10 @@
     @classmethod
     def setUpClass(cls):
         super(BaseNetworkTest, cls).setUpClass()
-        cls.isolated_creds = isolated_creds.IsolatedCreds(cls.__name__)
+        os = clients.Manager(interface=cls._interface)
+        cls.network_cfg = os.config.network
         if not cls.config.service_available.neutron:
             raise cls.skipException("Neutron support is required")
-        if cls.config.compute.allow_tenant_isolation:
-            creds = cls.isolated_creds.get_primary_creds()
-            username, tenant_name, password = creds
-            os = clients.Manager(username=username,
-                                 password=password,
-                                 tenant_name=tenant_name,
-                                 interface=cls._interface)
-        else:
-            os = clients.Manager(interface=cls._interface)
-        cls.network_cfg = os.config.network
         cls.client = os.network_client
         cls.networks = []
         cls.subnets = []
@@ -78,75 +68,39 @@
 
     @classmethod
     def tearDownClass(cls):
-        has_exception = False
+        # Clean up vpn services
         for vpnservice in cls.vpnservices:
-            try:
-                cls.client.delete_vpn_service(vpnservice['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
-
+            cls.client.delete_vpn_service(vpnservice['id'])
+        # Clean up routers
         for router in cls.routers:
-            try:
-                resp, body = cls.client.list_router_interfaces(router['id'])
-                interfaces = body['ports']
-                for i in interfaces:
-                    cls.client.remove_router_interface_with_subnet_id(
-                        router['id'], i['fixed_ips'][0]['subnet_id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
-            try:
-                cls.client.delete_router(router['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
-
+            resp, body = cls.client.list_router_interfaces(router['id'])
+            interfaces = body['ports']
+            for i in interfaces:
+                cls.client.remove_router_interface_with_subnet_id(
+                    router['id'], i['fixed_ips'][0]['subnet_id'])
+            cls.client.delete_router(router['id'])
+        # Clean up health monitors
         for health_monitor in cls.health_monitors:
-            try:
-                cls.client.delete_health_monitor(health_monitor['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
+            cls.client.delete_health_monitor(health_monitor['id'])
+        # Clean up members
         for member in cls.members:
-            try:
-                cls.client.delete_member(member['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
+            cls.client.delete_member(member['id'])
+        # Clean up vips
         for vip in cls.vips:
-            try:
-                cls.client.delete_vip(vip['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
+            cls.client.delete_vip(vip['id'])
+        # Clean up pools
         for pool in cls.pools:
-            try:
-                cls.client.delete_pool(pool['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
+            cls.client.delete_pool(pool['id'])
+        # Clean up ports
         for port in cls.ports:
-            try:
-                cls.client.delete_port(port['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
+            cls.client.delete_port(port['id'])
+        # Clean up subnets
         for subnet in cls.subnets:
-            try:
-                cls.client.delete_subnet(subnet['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
+            cls.client.delete_subnet(subnet['id'])
+        # Clean up networks
         for network in cls.networks:
-            try:
-                cls.client.delete_network(network['id'])
-            except Exception as exc:
-                LOG.exception(exc)
-                has_exception = True
+            cls.client.delete_network(network['id'])
         super(BaseNetworkTest, cls).tearDownClass()
-        if has_exception:
-            raise exceptions.TearDownException()
 
     @classmethod
     def create_network(cls, network_name=None):
diff --git a/tempest/api/network/common.py b/tempest/api/network/common.py
index c3fb821..43e7f68 100644
--- a/tempest/api/network/common.py
+++ b/tempest/api/network/common.py
@@ -56,7 +56,9 @@
 
 class DeletableSubnet(DeletableResource):
 
-    _router_ids = set()
+    def __init__(self, *args, **kwargs):
+        super(DeletableSubnet, self).__init__(*args, **kwargs)
+        self._router_ids = set()
 
     def add_to_router(self, router_id):
         self._router_ids.add(router_id)
diff --git a/tempest/api/object_storage/test_account_services.py b/tempest/api/object_storage/test_account_services.py
index 1a3f775..90b0914 100644
--- a/tempest/api/object_storage/test_account_services.py
+++ b/tempest/api/object_storage/test_account_services.py
@@ -101,7 +101,7 @@
                       'limit': limit}
             resp, container_list = \
                 self.account_client.list_account_containers(params=params)
-            self.assertLessEqual(len(container_list), limit)
+            self.assertTrue(len(container_list) <= limit, str(container_list))
 
     @attr(type='smoke')
     def test_list_account_metadata(self):
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 8d05fd8..67406b0 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -81,6 +81,10 @@
     message = "Server %(server_id)s failed to build and is in ERROR status"
 
 
+class ImageKilledException(TempestException):
+    message = "Image %(image_id)s 'killed' while waiting for %(status)s"
+
+
 class AddImageException(TempestException):
     message = "Image %(image_id)s failed to become ACTIVE in the allotted time"
 
diff --git a/tempest/services/compute/json/aggregates_client.py b/tempest/services/compute/json/aggregates_client.py
index 75ce9ff..b7c6bf1 100644
--- a/tempest/services/compute/json/aggregates_client.py
+++ b/tempest/services/compute/json/aggregates_client.py
@@ -97,3 +97,14 @@
                                post_body, self.headers)
         body = json.loads(body)
         return resp, body['aggregate']
+
+    def set_metadata(self, aggregate_id, meta):
+        """Replaces the aggregate's existing metadata with new metadata."""
+        post_body = {
+            'metadata': meta,
+        }
+        post_body = json.dumps({'set_metadata': post_body})
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               post_body, self.headers)
+        body = json.loads(body)
+        return resp, body['aggregate']
diff --git a/tempest/services/compute/json/hosts_client.py b/tempest/services/compute/json/hosts_client.py
index 30a3f7b..f51879d 100644
--- a/tempest/services/compute/json/hosts_client.py
+++ b/tempest/services/compute/json/hosts_client.py
@@ -44,3 +44,39 @@
         resp, body = self.get("os-hosts/%s" % str(hostname))
         body = json.loads(body)
         return resp, body['host']
+
+    def update_host(self, hostname, **kwargs):
+        """Update a host."""
+
+        request_body = {
+            'status': None,
+            'maintenance_mode': None,
+        }
+        request_body.update(**kwargs)
+        request_body = json.dumps(request_body)
+
+        resp, body = self.put("os-hosts/%s" % str(hostname), request_body,
+                              self.headers)
+        body = json.loads(body)
+        return resp, body
+
+    def startup_host(self, hostname):
+        """Startup a host."""
+
+        resp, body = self.get("os-hosts/%s/startup" % str(hostname))
+        body = json.loads(body)
+        return resp, body['host']
+
+    def shutdown_host(self, hostname):
+        """Shutdown a host."""
+
+        resp, body = self.get("os-hosts/%s/shutdown" % str(hostname))
+        body = json.loads(body)
+        return resp, body['host']
+
+    def reboot_host(self, hostname):
+        """reboot a host."""
+
+        resp, body = self.get("os-hosts/%s/reboot" % str(hostname))
+        body = json.loads(body)
+        return resp, body['host']
diff --git a/tempest/services/compute/xml/aggregates_client.py b/tempest/services/compute/xml/aggregates_client.py
index 8ef0af6..5faaff5 100644
--- a/tempest/services/compute/xml/aggregates_client.py
+++ b/tempest/services/compute/xml/aggregates_client.py
@@ -21,6 +21,7 @@
 from tempest import exceptions
 from tempest.services.compute.xml.common import Document
 from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
 from tempest.services.compute.xml.common import xml_to_json
 
 
@@ -112,3 +113,18 @@
                                self.headers)
         aggregate = self._format_aggregate(etree.fromstring(body))
         return resp, aggregate
+
+    def set_metadata(self, aggregate_id, meta):
+        """Replaces the aggregate's existing metadata with new metadata."""
+        post_body = Element("set_metadata")
+        metadata = Element("metadata")
+        post_body.append(metadata)
+        for k, v in meta.items():
+            meta = Element(k)
+            meta.append(Text(v))
+            metadata.append(meta)
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               str(Document(post_body)),
+                               self.headers)
+        aggregate = self._format_aggregate(etree.fromstring(body))
+        return resp, aggregate
diff --git a/tempest/services/compute/xml/hosts_client.py b/tempest/services/compute/xml/hosts_client.py
index 9743143..f7d7b0a 100644
--- a/tempest/services/compute/xml/hosts_client.py
+++ b/tempest/services/compute/xml/hosts_client.py
@@ -18,6 +18,8 @@
 
 from lxml import etree
 from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
 from tempest.services.compute.xml.common import xml_to_json
 
 
@@ -47,3 +49,46 @@
         node = etree.fromstring(body)
         body = [xml_to_json(x) for x in node.getchildren()]
         return resp, body
+
+    def update_host(self, hostname, status=None, maintenance_mode=None,
+                    **kwargs):
+        """Update a host."""
+
+        request_body = Element(status=status,
+                               maintenance_mode=maintenance_mode)
+        if kwargs:
+            for k, v in kwargs.iteritem():
+                request_body.add_attr(k, v)
+        resp, body = self.put("os-hosts/%s" % str(hostname),
+                              str(Document(request_body)),
+                              self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body
+
+    def startup_host(self, hostname):
+        """Startup a host."""
+
+        resp, body = self.get("os-hosts/%s/startup" % str(hostname),
+                              self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body
+
+    def shutdown_host(self, hostname):
+        """Shutdown a host."""
+
+        resp, body = self.get("os-hosts/%s/shutdown" % str(hostname),
+                              self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body
+
+    def reboot_host(self, hostname):
+        """Reboot a host."""
+
+        resp, body = self.get("os-hosts/%s/reboot" % str(hostname),
+                              self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body
diff --git a/tempest/services/image/v1/json/image_client.py b/tempest/services/image/v1/json/image_client.py
index 1921d78..41b9e81 100644
--- a/tempest/services/image/v1/json/image_client.py
+++ b/tempest/services/image/v1/json/image_client.py
@@ -265,9 +265,12 @@
                 LOG.info('Value transition from "%s" to "%s"'
                          'in %d second(s).', old_value,
                          value, dtime)
-            if (value == status):
+            if value == status:
                 return value
 
+            if value == 'killed':
+                raise exceptions.ImageKilledException(image_id=image_id,
+                                                      status=value)
             if dtime > self.build_timeout:
                 message = ('Time Limit Exceeded! (%ds)'
                            'while waiting for %s, '
diff --git a/tempest/services/image/v2/json/image_client.py b/tempest/services/image/v2/json/image_client.py
index f0531ec..62b8ff6 100644
--- a/tempest/services/image/v2/json/image_client.py
+++ b/tempest/services/image/v2/json/image_client.py
@@ -124,3 +124,13 @@
         url = 'v2/images/%s/file' % image_id
         resp, body = self.get(url)
         return resp, body
+
+    def add_image_tag(self, image_id, tag):
+        url = 'v2/images/%s/tags/%s' % (image_id, tag)
+        resp, body = self.put(url, body=None, headers=self.headers)
+        return resp, body
+
+    def delete_image_tag(self, image_id, tag):
+        url = 'v2/images/%s/tags/%s' % (image_id, tag)
+        resp, _ = self.delete(url)
+        return resp
diff --git a/tempest/test.py b/tempest/test.py
index d665119..edba709 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -217,7 +217,8 @@
             os.environ.get('OS_LOG_CAPTURE') != '0'):
             log_format = '%(asctime)-15s %(message)s'
             self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
-                                                   format=log_format))
+                                                   format=log_format,
+                                                   level=None))
 
     @classmethod
     def get_client_manager(cls):
diff --git a/tools/skip_tracker.py b/tools/skip_tracker.py
index ffaf134..0ae3323 100755
--- a/tools/skip_tracker.py
+++ b/tools/skip_tracker.py
@@ -46,14 +46,24 @@
     test methods that have been decorated to skip because of
     a particular bug.
     """
-    results = []
+    results = {}
     debug("Searching in %s", start)
     for root, _dirs, files in os.walk(start):
         for name in files:
             if name.startswith('test_') and name.endswith('py'):
                 path = os.path.join(root, name)
                 debug("Searching in %s", path)
-                results += find_skips_in_file(path)
+                temp_result = find_skips_in_file(path)
+                for method_name, bug_no in temp_result:
+                    if results.get(bug_no):
+                        result_dict = results.get(bug_no)
+                        if result_dict.get(name):
+                            result_dict[name].append(method_name)
+                        else:
+                            result_dict[name] = [method_name]
+                        results[bug_no] = result_dict
+                    else:
+                        results[bug_no] = {name: [method_name]}
     return results
 
 
@@ -83,11 +93,19 @@
     return results
 
 
+def get_results(result_dict):
+    results = []
+    for bug_no in result_dict.keys():
+        for method in result_dict[bug_no]:
+            results.append((method, bug_no))
+    return results
+
+
 if __name__ == '__main__':
     logging.basicConfig(format='%(levelname)s: %(message)s',
                         level=logging.INFO)
     results = find_skips()
-    unique_bugs = sorted(set([bug for (method, bug) in results]))
+    unique_bugs = sorted(set([bug for (method, bug) in get_results(results)]))
     unskips = []
     duplicates = []
     info("Total bug skips found: %d", len(results))
@@ -122,4 +140,7 @@
         print("should be removed from the test cases:")
         print()
         for bug in unskips:
-            print("  %7s" % bug)
+            message = "  %7s in " % bug
+            locations = ["%s" % x for x in results[bug].keys()]
+            message += " and ".join(locations)
+            print(message)