Merge "Add a run_tempest.sh script"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index f306d8e..d6d0964 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -95,6 +95,17 @@
 #syslog_log_facility=LOG_USER
 
 
+[baremetal]
+
+#
+# Options defined in tempest.config
+#
+
+# Catalog type of the baremetal provisioning service. (string
+# value)
+#catalog_type=baremetal
+
+
 [boto]
 
 #
@@ -672,6 +683,10 @@
 # value)
 #savanna=false
 
+# Whether or not Ironic is expected to be available (boolean
+# value)
+#ironic=false
+
 
 [stress]
 
diff --git a/requirements.txt b/requirements.txt
index cd11aa7..0c4a659 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,11 +14,11 @@
 python-neutronclient>=2.3.0,<3
 python-cinderclient>=1.0.6
 python-heatclient>=0.2.3
+python-swiftclient>=1.5
 testresources>=0.2.4
 keyring>=1.6.1,<2.0
 testrepository>=0.0.17
 oslo.config>=1.2.0
-eventlet>=0.13.0
 six>=1.4.1
 iso8601>=0.1.8
 fixtures>=0.3.14
diff --git a/tempest/api/baremetal/__init__.py b/tempest/api/baremetal/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api/baremetal/__init__.py
diff --git a/tempest/api/baremetal/base.py b/tempest/api/baremetal/base.py
new file mode 100644
index 0000000..3aad1b5
--- /dev/null
+++ b/tempest/api/baremetal/base.py
@@ -0,0 +1,171 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 functools
+
+from tempest import clients
+from tempest.common.utils import data_utils
+from tempest import exceptions as exc
+from tempest import test
+
+
+def creates(resource):
+    """Decorator that adds resources to the appropriate cleanup list."""
+
+    def decorator(f):
+        @functools.wraps(f)
+        def wrapper(cls, *args, **kwargs):
+            result = f(cls, *args, **kwargs)
+            body = result[resource]
+
+            if 'uuid' in body:
+                cls.created_objects[resource].add(body['uuid'])
+
+            return result
+        return wrapper
+    return decorator
+
+
+class BaseBaremetalTest(test.BaseTestCase):
+    """Base class for Baremetal API tests."""
+
+    @classmethod
+    def setUpClass(cls):
+        super(BaseBaremetalTest, cls).setUpClass()
+
+        if not cls.config.service_available.ironic:
+            skip_msg = ('%s skipped as Ironic is not available' % cls.__name__)
+            raise cls.skipException(skip_msg)
+
+        mgr = clients.AdminManager()
+        cls.client = mgr.baremetal_client
+
+        cls.created_objects = {'chassis': set(),
+                               'port': set(),
+                               'node': set()}
+
+    @classmethod
+    def tearDownClass(cls):
+        """Ensure that all created objects get destroyed."""
+
+        try:
+            for resource, uuids in cls.created_objects.iteritems():
+                delete_method = getattr(cls.client, 'delete_%s' % resource)
+                for u in uuids:
+                    delete_method(u, ignore_errors=exc.NotFound)
+        finally:
+            super(BaseBaremetalTest, cls).tearDownClass()
+
+    @classmethod
+    @creates('chassis')
+    def create_chassis(cls, description=None, expect_errors=False):
+        """
+        Wrapper utility for creating test chassis.
+
+        :param description: A description of the chassis. if not supplied,
+            a random value will be generated.
+        :return: Created chassis.
+
+        """
+        description = description or data_utils.rand_name('test-chassis-')
+        resp, body = cls.client.create_chassis(description=description)
+
+        return {'chassis': body, 'response': resp}
+
+    @classmethod
+    @creates('node')
+    def create_node(cls, chassis_id, cpu_arch='x86', cpu_num=8, storage=1024,
+                    memory=4096, driver='fake'):
+        """
+        Wrapper utility for creating test baremetal nodes.
+
+        :param cpu_arch: CPU architecture of the node. Default: x86.
+        :param cpu_num: Number of CPUs. Default: 8.
+        :param storage: Disk size. Default: 1024.
+        :param memory: Available RAM. Default: 4096.
+        :return: Created node.
+
+        """
+        resp, body = cls.client.create_node(chassis_id, cpu_arch=cpu_arch,
+                                            cpu_num=cpu_num, storage=storage,
+                                            memory=memory, driver=driver)
+
+        return {'node': body, 'response': resp}
+
+    @classmethod
+    @creates('port')
+    def create_port(cls, node_id, address=None):
+        """
+        Wrapper utility for creating test ports.
+
+        :param address: MAC address of the port. If not supplied, a random
+            value will be generated.
+        :return: Created port.
+
+        """
+        address = address or data_utils.rand_mac_address()
+        resp, body = cls.client.create_port(address=address, node_id=node_id)
+
+        return {'port': body, 'response': resp}
+
+    @classmethod
+    def delete_chassis(cls, chassis_id):
+        """
+        Deletes a chassis having the specified UUID.
+
+        :param uuid: The unique identifier of the chassis.
+        :return: Server response.
+
+        """
+
+        resp, body = cls.client.delete_chassis(chassis_id)
+
+        if chassis_id in cls.created_objects['chassis']:
+            cls.created_objects['chassis'].remove(chassis_id)
+
+        return resp
+
+    @classmethod
+    def delete_node(cls, node_id):
+        """
+        Deletes a node having the specified UUID.
+
+        :param uuid: The unique identifier of the node.
+        :return: Server response.
+
+        """
+
+        resp, body = cls.client.delete_node(node_id)
+
+        if node_id in cls.created_objects['node']:
+            cls.created_objects['node'].remove(node_id)
+
+        return resp
+
+    @classmethod
+    def delete_port(cls, port_id):
+        """
+        Deletes a port having the specified UUID.
+
+        :param uuid: The unique identifier of the port.
+        :return: Server response.
+
+        """
+
+        resp, body = cls.client.delete_port(port_id)
+
+        if port_id in cls.created_objects['port']:
+            cls.created_objects['port'].remove(port_id)
+
+        return resp
diff --git a/tempest/api/baremetal/test_api_discovery.py b/tempest/api/baremetal/test_api_discovery.py
new file mode 100644
index 0000000..32f3d50
--- /dev/null
+++ b/tempest/api/baremetal/test_api_discovery.py
@@ -0,0 +1,46 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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.baremetal import base
+from tempest import test
+
+
+class TestApiDiscovery(base.BaseBaremetalTest):
+    """Tests for API discovery features."""
+
+    @test.attr(type='smoke')
+    def test_api_versions(self):
+        resp, descr = self.client.get_api_description()
+        expected_versions = ('v1',)
+
+        versions = [version['id'] for version in descr['versions']]
+
+        for v in expected_versions:
+            self.assertIn(v, versions)
+
+    @test.attr(type='smoke')
+    def test_default_version(self):
+        resp, descr = self.client.get_api_description()
+        default_version = descr['default_version']
+
+        self.assertEqual(default_version['id'], 'v1')
+
+    @test.attr(type='smoke')
+    def test_version_1_resources(self):
+        resp, descr = self.client.get_version_description(version='v1')
+        expected_resources = ('nodes', 'chassis',
+                              'ports', 'links', 'media_types')
+
+        for res in expected_resources:
+            self.assertIn(res, descr)
diff --git a/tempest/api/baremetal/test_chassis.py b/tempest/api/baremetal/test_chassis.py
new file mode 100644
index 0000000..35a93ca
--- /dev/null
+++ b/tempest/api/baremetal/test_chassis.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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.baremetal import base
+from tempest.common.utils import data_utils
+from tempest import exceptions as exc
+from tempest import test
+
+
+class TestChassis(base.BaseBaremetalTest):
+    """Tests for chassis."""
+
+    @test.attr(type='smoke')
+    def test_create_chassis(self):
+        descr = data_utils.rand_name('test-chassis-')
+        ch = self.create_chassis(description=descr)['chassis']
+
+        self.assertEqual(ch['description'], descr)
+
+    @test.attr(type='smoke')
+    def test_create_chassis_unicode_description(self):
+        # Use a unicode string for testing:
+        # 'We ♡ OpenStack in Ukraine'
+        descr = u'В Україні ♡ OpenStack!'
+        ch = self.create_chassis(description=descr)['chassis']
+
+        self.assertEqual(ch['description'], descr)
+
+    @test.attr(type='smoke')
+    def test_show_chassis(self):
+        descr = data_utils.rand_name('test-chassis-')
+        uuid = self.create_chassis(description=descr)['chassis']['uuid']
+
+        resp, chassis = self.client.show_chassis(uuid)
+
+        self.assertEqual(chassis['uuid'], uuid)
+        self.assertEqual(chassis['description'], descr)
+
+    @test.attr(type="smoke")
+    def test_list_chassis(self):
+        created_ids = [self.create_chassis()['chassis']['uuid']
+                       for i in range(0, 5)]
+
+        resp, body = self.client.list_chassis()
+        loaded_ids = [ch['uuid'] for ch in body['chassis']]
+
+        for i in created_ids:
+            self.assertIn(i, loaded_ids)
+
+    @test.attr(type='smoke')
+    def test_delete_chassis(self):
+        uuid = self.create_chassis()['chassis']['uuid']
+
+        self.delete_chassis(uuid)
+
+        self.assertRaises(exc.NotFound, self.client.show_chassis, uuid)
+
+    @test.attr(type='smoke')
+    def test_update_chassis(self):
+        chassis_id = self.create_chassis()['chassis']['uuid']
+
+        new_description = data_utils.rand_name('new-description-')
+        self.client.update_chassis(chassis_id, description=new_description)
+
+        resp, chassis = self.client.show_chassis(chassis_id)
+        self.assertEqual(chassis['description'], new_description)
diff --git a/tempest/api/baremetal/test_nodes.py b/tempest/api/baremetal/test_nodes.py
new file mode 100644
index 0000000..f9b65ed
--- /dev/null
+++ b/tempest/api/baremetal/test_nodes.py
@@ -0,0 +1,97 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 six
+
+from tempest.api.baremetal import base
+from tempest import exceptions as exc
+from tempest import test
+
+
+class TestNodes(base.BaseBaremetalTest):
+    '''Tests for baremetal nodes.'''
+
+    def setUp(self):
+        super(TestNodes, self).setUp()
+
+        self.chassis = self.create_chassis()['chassis']
+
+    @test.attr(type='smoke')
+    def test_create_node(self):
+        params = {'cpu_arch': 'x86_64',
+                  'cpu_num': '12',
+                  'storage': '10240',
+                  'memory': '1024'}
+
+        node = self.create_node(self.chassis['uuid'], **params)['node']
+
+        for key in params:
+            self.assertEqual(node['properties'][key], params[key])
+
+    @test.attr(type='smoke')
+    def test_delete_node(self):
+        node = self.create_node(self.chassis['uuid'])['node']
+        node_id = node['uuid']
+
+        resp = self.delete_node(node_id)
+
+        self.assertEqual(resp['status'], '204')
+        self.assertRaises(exc.NotFound, self.client.show_node, node_id)
+
+    @test.attr(type='smoke')
+    def test_show_node(self):
+        params = {'cpu_arch': 'x86_64',
+                  'cpu_num': '4',
+                  'storage': '100',
+                  'memory': '512'}
+
+        created_node = self.create_node(self.chassis['uuid'], **params)['node']
+        resp, loaded_node = self.client.show_node(created_node['uuid'])
+
+        for key, val in created_node.iteritems():
+            if key not in ('created_at', 'updated_at'):
+                self.assertEqual(loaded_node[key], val)
+
+    @test.attr(type='smoke')
+    def test_list_nodes(self):
+        uuids = [self.create_node(self.chassis['uuid'])['node']['uuid']
+                 for i in range(0, 5)]
+
+        resp, body = self.client.list_nodes()
+        loaded_uuids = [n['uuid'] for n in body['nodes']]
+
+        for u in uuids:
+            self.assertIn(u, loaded_uuids)
+
+    @test.attr(type='smoke')
+    def test_update_node(self):
+        props = {'cpu_arch': 'x86_64',
+                 'cpu_num': '12',
+                 'storage': '10',
+                 'memory': '128'}
+
+        node = self.create_node(self.chassis['uuid'], **props)['node']
+        node_id = node['uuid']
+
+        new_props = {'cpu_arch': 'x86',
+                     'cpu_num': '1',
+                     'storage': '10000',
+                     'memory': '12300'}
+
+        self.client.update_node(node_id, properties=new_props)
+        resp, node = self.client.show_node(node_id)
+
+        for name, value in six.iteritems(new_props):
+            if name not in ('created_at', 'updated_at'):
+                self.assertEqual(node['properties'][name], value)
diff --git a/tempest/api/baremetal/test_ports.py b/tempest/api/baremetal/test_ports.py
new file mode 100644
index 0000000..8249705
--- /dev/null
+++ b/tempest/api/baremetal/test_ports.py
@@ -0,0 +1,85 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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.baremetal import base
+from tempest.common.utils import data_utils
+from tempest import exceptions as exc
+from tempest import test
+
+
+class TestPorts(base.BaseBaremetalTest):
+    """Tests for ports."""
+
+    def setUp(self):
+        super(TestPorts, self).setUp()
+
+        chassis = self.create_chassis()['chassis']
+        self.node = self.create_node(chassis['uuid'])['node']
+
+    @test.attr(type='smoke')
+    def test_create_port(self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        port = self.create_port(node_id=node_id, address=address)['port']
+
+        self.assertEqual(port['address'], address)
+        self.assertEqual(port['node_uuid'], node_id)
+
+    @test.attr(type='smoke')
+    def test_delete_port(self):
+        node_id = self.node['uuid']
+        port_id = self.create_port(node_id=node_id)['port']['uuid']
+
+        resp = self.delete_port(port_id)
+
+        self.assertEqual(resp['status'], '204')
+        self.assertRaises(exc.NotFound, self.client.show_port, port_id)
+
+    @test.attr(type='smoke')
+    def test_show_port(self):
+        node_id = self.node['uuid']
+        address = data_utils.rand_mac_address()
+
+        port_id = self.create_port(node_id=node_id,
+                                   address=address)['port']['uuid']
+
+        resp, port = self.client.show_port(port_id)
+
+        self.assertEqual(port['uuid'], port_id)
+        self.assertEqual(port['address'], address)
+
+    @test.attr(type='smoke')
+    def test_list_ports(self):
+        node_id = self.node['uuid']
+
+        uuids = [self.create_port(node_id=node_id)['port']['uuid']
+                 for i in range(0, 5)]
+
+        resp, body = self.client.list_ports()
+        loaded_uuids = [p['uuid'] for p in body['ports']]
+
+        for u in uuids:
+            self.assertIn(u, loaded_uuids)
+
+    @test.attr(type='smoke')
+    def test_update_port(self):
+        node_id = self.node['uuid']
+        port_id = self.create_port(node_id=node_id)['port']['uuid']
+
+        new_address = data_utils.rand_mac_address()
+        self.client.update_port(port_id, address=new_address)
+
+        resp, body = self.client.show_port(port_id)
+        self.assertEqual(body['address'], new_address)
diff --git a/tempest/api/baremetal/test_ports_negative.py b/tempest/api/baremetal/test_ports_negative.py
new file mode 100644
index 0000000..423313cb
--- /dev/null
+++ b/tempest/api/baremetal/test_ports_negative.py
@@ -0,0 +1,42 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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.baremetal import base
+from tempest.common.utils import data_utils
+from tempest import exceptions as exc
+from tempest import test
+
+
+class TestPortsNegative(base.BaseBaremetalTest):
+    """Negative tests for ports."""
+
+    def setUp(self):
+        super(TestPortsNegative, self).setUp()
+
+        chassis = self.create_chassis()['chassis']
+        self.node = self.create_node(chassis['uuid'])['node']
+
+    @test.attr(type='negative')
+    def test_create_port_invalid_mac(self):
+        node_id = self.node['uuid']
+        address = 'not an uuid'
+
+        self.assertRaises(exc.BadRequest,
+                          self.create_port, node_id=node_id, address=address)
+
+    @test.attr(type='negative')
+    def test_create_port_wrong_node_id(self):
+        node_id = str(data_utils.rand_uuid())
+
+        self.assertRaises(exc.BadRequest, self.create_port, node_id=node_id)
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index b060f15..311e158 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -130,12 +130,22 @@
             r, b = cls.servers_client.list_servers()
             servers = [s for s in b['servers'] if s['name'].startswith(name)]
 
-        cls.servers.extend(servers)
-
         if 'wait_until' in kwargs:
             for server in servers:
-                cls.servers_client.wait_for_server_status(
-                    server['id'], kwargs['wait_until'])
+                try:
+                    cls.servers_client.wait_for_server_status(
+                        server['id'], kwargs['wait_until'])
+                except Exception as ex:
+                    if ('preserve_server_on_error' not in kwargs
+                        or kwargs['preserve_server_on_error'] is False):
+                        for server in servers:
+                            try:
+                                cls.servers_client.delete_server(server['id'])
+                            except Exception:
+                                pass
+                    raise ex
+
+        cls.servers.extend(servers)
 
         return resp, body
 
diff --git a/tempest/api/compute/limits/test_absolute_limits.py b/tempest/api/compute/limits/test_absolute_limits.py
index 2809244..908d537 100644
--- a/tempest/api/compute/limits/test_absolute_limits.py
+++ b/tempest/api/compute/limits/test_absolute_limits.py
@@ -16,8 +16,7 @@
 #    under the License.
 
 from tempest.api.compute import base
-from tempest import exceptions
-from tempest.test import attr
+from tempest import test
 
 
 class AbsoluteLimitsTestJSON(base.BaseV2ComputeTest):
@@ -27,9 +26,8 @@
     def setUpClass(cls):
         super(AbsoluteLimitsTestJSON, cls).setUpClass()
         cls.client = cls.limits_client
-        cls.server_client = cls.servers_client
 
-    @attr(type='gate')
+    @test.attr(type='gate')
     def test_absLimits_get(self):
         # To check if all limits are present in the response
         resp, absolute_limits = self.client.get_absolute_limits()
@@ -49,25 +47,6 @@
                          "Failed to find element %s in absolute limits list"
                          % ', '.join(ele for ele in missing_elements))
 
-    @attr(type=['negative', 'gate'])
-    def test_max_image_meta_exceed_limit(self):
-        # We should not create vm with image meta over maxImageMeta limit
-        # Get max limit value
-        max_meta = self.client.get_specific_absolute_limit('maxImageMeta')
-
-        # Create server should fail, since we are passing > metadata Limit!
-        max_meta_data = int(max_meta) + 1
-
-        meta_data = {}
-        for xx in range(max_meta_data):
-            meta_data[str(xx)] = str(xx)
-
-        self.assertRaises(exceptions.OverLimit,
-                          self.server_client.create_server,
-                          name='test', meta=meta_data,
-                          flavor_ref=self.flavor_ref,
-                          image_ref=self.image_ref)
-
 
 class AbsoluteLimitsTestXML(AbsoluteLimitsTestJSON):
     _interface = 'xml'
diff --git a/tempest/api/compute/limits/test_absolute_limits_negative.py b/tempest/api/compute/limits/test_absolute_limits_negative.py
new file mode 100644
index 0000000..8547403
--- /dev/null
+++ b/tempest/api/compute/limits/test_absolute_limits_negative.py
@@ -0,0 +1,53 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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.compute import base
+from tempest import exceptions
+from tempest import test
+
+
+class AbsoluteLimitsNegativeTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(AbsoluteLimitsNegativeTestJSON, cls).setUpClass()
+        cls.client = cls.limits_client
+        cls.server_client = cls.servers_client
+
+    @test.attr(type=['negative', 'gate'])
+    def test_max_image_meta_exceed_limit(self):
+        # We should not create vm with image meta over maxImageMeta limit
+        # Get max limit value
+        max_meta = self.client.get_specific_absolute_limit('maxImageMeta')
+
+        # Create server should fail, since we are passing > metadata Limit!
+        max_meta_data = int(max_meta) + 1
+
+        meta_data = {}
+        for xx in range(max_meta_data):
+            meta_data[str(xx)] = str(xx)
+
+        self.assertRaises(exceptions.OverLimit,
+                          self.server_client.create_server,
+                          name='test', meta=meta_data,
+                          flavor_ref=self.flavor_ref,
+                          image_ref=self.image_ref)
+
+
+class AbsoluteLimitsNegativeTestXML(AbsoluteLimitsNegativeTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/security_groups/test_security_groups.py b/tempest/api/compute/security_groups/test_security_groups.py
index 95e9171..4ae65be 100644
--- a/tempest/api/compute/security_groups/test_security_groups.py
+++ b/tempest/api/compute/security_groups/test_security_groups.py
@@ -15,17 +15,10 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import testtools
-import uuid
-
 from tempest.api.compute import base
 from tempest.common.utils import data_utils
-from tempest import config
 from tempest import exceptions
-from tempest.test import attr
-from tempest.test import skip_because
-
-CONF = config.CONF
+from tempest import test
 
 
 class SecurityGroupsTestJSON(base.BaseV2ComputeTest):
@@ -35,13 +28,12 @@
     def setUpClass(cls):
         super(SecurityGroupsTestJSON, cls).setUpClass()
         cls.client = cls.security_groups_client
-        cls.neutron_available = cls.config.service_available.neutron
 
     def _delete_security_group(self, securitygroup_id):
         resp, _ = self.client.delete_security_group(securitygroup_id)
         self.assertEqual(202, resp.status)
 
-    @attr(type='gate')
+    @test.attr(type='gate')
     def test_security_groups_create_list_delete(self):
         # Positive test:Should return the list of Security Groups
         # Create 3 Security Groups
@@ -69,7 +61,7 @@
 
     # TODO(afazekas): scheduled for delete,
     # test_security_group_create_get_delete covers it
-    @attr(type='gate')
+    @test.attr(type='gate')
     def test_security_group_create_delete(self):
         # Security Group should be created, verified and deleted
         s_name = data_utils.rand_name('securitygroup-')
@@ -88,7 +80,7 @@
                          "The created Security Group name is "
                          "not equal to the requested name")
 
-    @attr(type='gate')
+    @test.attr(type='gate')
     def test_security_group_create_get_delete(self):
         # Security Group should be created, fetched and deleted
         s_name = data_utils.rand_name('securitygroup-')
@@ -112,121 +104,7 @@
                          "The fetched Security Group is different "
                          "from the created Group")
 
-    @attr(type=['negative', 'smoke'])
-    def test_security_group_get_nonexistant_group(self):
-        # Negative test:Should not be able to GET the details
-        # of non-existent Security Group
-        security_group_id = []
-        resp, body = self.client.list_security_groups()
-        for i in range(len(body)):
-            security_group_id.append(body[i]['id'])
-        # Creating a non-existent Security Group id
-        while True:
-            non_exist_id = data_utils.rand_int_id(start=999)
-            if self.neutron_available:
-                non_exist_id = str(uuid.uuid4())
-            if non_exist_id not in security_group_id:
-                break
-        self.assertRaises(exceptions.NotFound, self.client.get_security_group,
-                          non_exist_id)
-
-    @skip_because(bug="1161411",
-                  condition=CONF.service_available.neutron)
-    @attr(type=['negative', 'gate'])
-    def test_security_group_create_with_invalid_group_name(self):
-        # Negative test: Security Group should not be created with group name
-        # as an empty string/with white spaces/chars more than 255
-        s_description = data_utils.rand_name('description-')
-        # Create Security Group with empty string as group name
-        self.assertRaises(exceptions.BadRequest,
-                          self.client.create_security_group, "", s_description)
-        # Create Security Group with white space in group name
-        self.assertRaises(exceptions.BadRequest,
-                          self.client.create_security_group, " ",
-                          s_description)
-        # Create Security Group with group name longer than 255 chars
-        s_name = 'securitygroup-'.ljust(260, '0')
-        self.assertRaises(exceptions.BadRequest,
-                          self.client.create_security_group, s_name,
-                          s_description)
-
-    @skip_because(bug="1161411",
-                  condition=CONF.service_available.neutron)
-    @attr(type=['negative', 'gate'])
-    def test_security_group_create_with_invalid_group_description(self):
-        # Negative test:Security Group should not be created with description
-        # as an empty string/with white spaces/chars more than 255
-        s_name = data_utils.rand_name('securitygroup-')
-        # Create Security Group with empty string as description
-        self.assertRaises(exceptions.BadRequest,
-                          self.client.create_security_group, s_name, "")
-        # Create Security Group with white space in description
-        self.assertRaises(exceptions.BadRequest,
-                          self.client.create_security_group, s_name, " ")
-        # Create Security Group with group description longer than 255 chars
-        s_description = 'description-'.ljust(260, '0')
-        self.assertRaises(exceptions.BadRequest,
-                          self.client.create_security_group, s_name,
-                          s_description)
-
-    @testtools.skipIf(CONF.service_available.neutron,
-                      "Neutron allows duplicate names for security groups")
-    @attr(type=['negative', 'gate'])
-    def test_security_group_create_with_duplicate_name(self):
-        # Negative test:Security Group with duplicate name should not
-        # be created
-        s_name = data_utils.rand_name('securitygroup-')
-        s_description = data_utils.rand_name('description-')
-        resp, security_group =\
-            self.client.create_security_group(s_name, s_description)
-        self.assertEqual(200, resp.status)
-
-        self.addCleanup(self.client.delete_security_group,
-                        security_group['id'])
-        # Now try the Security Group with the same 'Name'
-        self.assertRaises(exceptions.BadRequest,
-                          self.client.create_security_group, s_name,
-                          s_description)
-
-    @attr(type=['negative', 'gate'])
-    def test_delete_the_default_security_group(self):
-        # Negative test:Deletion of the "default" Security Group should Fail
-        default_security_group_id = None
-        resp, body = self.client.list_security_groups()
-        for i in range(len(body)):
-            if body[i]['name'] == 'default':
-                default_security_group_id = body[i]['id']
-                break
-        # Deleting the "default" Security Group
-        self.assertRaises(exceptions.BadRequest,
-                          self.client.delete_security_group,
-                          default_security_group_id)
-
-    @attr(type=['negative', 'smoke'])
-    def test_delete_nonexistant_security_group(self):
-        # Negative test:Deletion of a non-existent Security Group should Fail
-        security_group_id = []
-        resp, body = self.client.list_security_groups()
-        for i in range(len(body)):
-            security_group_id.append(body[i]['id'])
-        # Creating non-existent Security Group
-        while True:
-            non_exist_id = data_utils.rand_int_id(start=999)
-            if self.neutron_available:
-                non_exist_id = str(uuid.uuid4())
-            if non_exist_id not in security_group_id:
-                break
-        self.assertRaises(exceptions.NotFound,
-                          self.client.delete_security_group, non_exist_id)
-
-    @attr(type=['negative', 'gate'])
-    def test_delete_security_group_without_passing_id(self):
-        # Negative test:Deletion of a Security Group with out passing ID
-        # should Fail
-        self.assertRaises(exceptions.NotFound,
-                          self.client.delete_security_group, '')
-
-    @attr(type='gate')
+    @test.attr(type='gate')
     def test_server_security_groups(self):
         # Checks that security groups may be added and linked to a server
         # and not deleted if the server is active.
@@ -282,6 +160,33 @@
         self.client.delete_security_group(sg2_id)
         self.assertEqual(202, resp.status)
 
+    @test.attr(type='gate')
+    def test_update_security_groups(self):
+        # Update security group name and description
+        # Create a security group
+        s_name = data_utils.rand_name('sg-')
+        s_description = data_utils.rand_name('description-')
+        resp, securitygroup = \
+            self.client.create_security_group(s_name, s_description)
+        self.assertEqual(200, resp.status)
+        self.assertIn('id', securitygroup)
+        securitygroup_id = securitygroup['id']
+        self.addCleanup(self._delete_security_group,
+                        securitygroup_id)
+        # Update the name and description
+        s_new_name = data_utils.rand_name('sg-hth-')
+        s_new_des = data_utils.rand_name('description-hth-')
+        resp, sg_new = \
+            self.client.update_security_group(securitygroup_id,
+                                              name=s_new_name,
+                                              description=s_new_des)
+        self.assertEqual(200, resp.status)
+        # get the security group
+        resp, fetched_group = \
+            self.client.get_security_group(securitygroup_id)
+        self.assertEqual(s_new_name, fetched_group['name'])
+        self.assertEqual(s_new_des, fetched_group['description'])
+
 
 class SecurityGroupsTestXML(SecurityGroupsTestJSON):
     _interface = 'xml'
diff --git a/tempest/api/compute/security_groups/test_security_groups_negative.py b/tempest/api/compute/security_groups/test_security_groups_negative.py
new file mode 100644
index 0000000..6a8e604
--- /dev/null
+++ b/tempest/api/compute/security_groups/test_security_groups_negative.py
@@ -0,0 +1,216 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Huawei Technologies Co.,LTD.
+# 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 testtools
+
+from tempest.api.compute import base
+from tempest.common.utils import data_utils
+from tempest import config
+from tempest import exceptions
+from tempest import test
+
+CONF = config.CONF
+
+
+class SecurityGroupsNegativeTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(SecurityGroupsNegativeTestJSON, cls).setUpClass()
+        cls.client = cls.security_groups_client
+        cls.neutron_available = cls.config.service_available.neutron
+
+    def _delete_security_group(self, securitygroup_id):
+        resp, _ = self.client.delete_security_group(securitygroup_id)
+        self.assertEqual(202, resp.status)
+
+    def _generate_a_non_existent_security_group_id(self):
+        security_group_id = []
+        resp, body = self.client.list_security_groups()
+        for i in range(len(body)):
+            security_group_id.append(body[i]['id'])
+        # Generate a non-existent security group id
+        while True:
+            non_exist_id = data_utils.rand_int_id(start=999)
+            if self.neutron_available:
+                non_exist_id = data_utils.rand_uuid()
+            if non_exist_id not in security_group_id:
+                break
+        return non_exist_id
+
+    @test.attr(type=['negative', 'smoke'])
+    def test_security_group_get_nonexistent_group(self):
+        # Negative test:Should not be able to GET the details
+        # of non-existent Security Group
+        non_exist_id = self._generate_a_non_existent_security_group_id()
+        self.assertRaises(exceptions.NotFound, self.client.get_security_group,
+                          non_exist_id)
+
+    @test.skip_because(bug="1161411",
+                       condition=CONF.service_available.neutron)
+    @test.attr(type=['negative', 'gate'])
+    def test_security_group_create_with_invalid_group_name(self):
+        # Negative test: Security Group should not be created with group name
+        # as an empty string/with white spaces/chars more than 255
+        s_description = data_utils.rand_name('description-')
+        # Create Security Group with empty string as group name
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_security_group, "", s_description)
+        # Create Security Group with white space in group name
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_security_group, " ",
+                          s_description)
+        # Create Security Group with group name longer than 255 chars
+        s_name = 'securitygroup-'.ljust(260, '0')
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_security_group, s_name,
+                          s_description)
+
+    @test.skip_because(bug="1161411",
+                       condition=CONF.service_available.neutron)
+    @test.attr(type=['negative', 'gate'])
+    def test_security_group_create_with_invalid_group_description(self):
+        # Negative test:Security Group should not be created with description
+        # as an empty string/with white spaces/chars more than 255
+        s_name = data_utils.rand_name('securitygroup-')
+        # Create Security Group with empty string as description
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_security_group, s_name, "")
+        # Create Security Group with white space in description
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_security_group, s_name, " ")
+        # Create Security Group with group description longer than 255 chars
+        s_description = 'description-'.ljust(260, '0')
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_security_group, s_name,
+                          s_description)
+
+    @testtools.skipIf(CONF.service_available.neutron,
+                      "Neutron allows duplicate names for security groups")
+    @test.attr(type=['negative', 'gate'])
+    def test_security_group_create_with_duplicate_name(self):
+        # Negative test:Security Group with duplicate name should not
+        # be created
+        s_name = data_utils.rand_name('securitygroup-')
+        s_description = data_utils.rand_name('description-')
+        resp, security_group =\
+            self.client.create_security_group(s_name, s_description)
+        self.assertEqual(200, resp.status)
+
+        self.addCleanup(self.client.delete_security_group,
+                        security_group['id'])
+        # Now try the Security Group with the same 'Name'
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_security_group, s_name,
+                          s_description)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_delete_the_default_security_group(self):
+        # Negative test:Deletion of the "default" Security Group should Fail
+        default_security_group_id = None
+        resp, body = self.client.list_security_groups()
+        for i in range(len(body)):
+            if body[i]['name'] == 'default':
+                default_security_group_id = body[i]['id']
+                break
+        # Deleting the "default" Security Group
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.delete_security_group,
+                          default_security_group_id)
+
+    @test.attr(type=['negative', 'smoke'])
+    def test_delete_nonexistent_security_group(self):
+        # Negative test:Deletion of a non-existent Security Group should fail
+        non_exist_id = self._generate_a_non_existent_security_group_id()
+        self.assertRaises(exceptions.NotFound,
+                          self.client.delete_security_group, non_exist_id)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_delete_security_group_without_passing_id(self):
+        # Negative test:Deletion of a Security Group with out passing ID
+        # should Fail
+        self.assertRaises(exceptions.NotFound,
+                          self.client.delete_security_group, '')
+
+    @testtools.skipIf(CONF.service_available.neutron,
+                      "Neutron not check the security_group_id")
+    @test.attr(type=['negative', 'gate'])
+    def test_update_security_group_with_invalid_sg_id(self):
+        # Update security_group with invalid sg_id should fail
+        s_name = data_utils.rand_name('sg-')
+        s_description = data_utils.rand_name('description-')
+        # Create a non int sg_id
+        sg_id_invalid = data_utils.rand_name('sg-')
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_security_group, sg_id_invalid,
+                          name=s_name, description=s_description)
+
+    @testtools.skipIf(CONF.service_available.neutron,
+                      "Neutron not check the security_group_name")
+    @test.attr(type=['negative', 'gate'])
+    def test_update_security_group_with_invalid_sg_name(self):
+        # Update security_group with invalid sg_name should fail
+        s_name = data_utils.rand_name('sg-')
+        s_description = data_utils.rand_name('description-')
+        resp, securitygroup = \
+            self.client.create_security_group(s_name, s_description)
+        self.assertEqual(200, resp.status)
+        self.assertIn('id', securitygroup)
+        securitygroup_id = securitygroup['id']
+        self.addCleanup(self._delete_security_group,
+                        securitygroup_id)
+        # Update Security Group with group name longer than 255 chars
+        s_new_name = 'securitygroup-'.ljust(260, '0')
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_security_group,
+                          securitygroup_id, name=s_new_name)
+
+    @testtools.skipIf(CONF.service_available.neutron,
+                      "Neutron not check the security_group_description")
+    @test.attr(type=['negative', 'gate'])
+    def test_update_security_group_with_invalid_sg_des(self):
+        # Update security_group with invalid sg_des should fail
+        s_name = data_utils.rand_name('sg-')
+        s_description = data_utils.rand_name('description-')
+        resp, securitygroup = \
+            self.client.create_security_group(s_name, s_description)
+        self.assertEqual(200, resp.status)
+        self.assertIn('id', securitygroup)
+        securitygroup_id = securitygroup['id']
+        self.addCleanup(self._delete_security_group,
+                        securitygroup_id)
+        # Update Security Group with group description longer than 255 chars
+        s_new_des = 'des-'.ljust(260, '0')
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_security_group,
+                          securitygroup_id, description=s_new_des)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_update_non_existent_security_group(self):
+        # Update a non-existent Security Group should Fail
+        non_exist_id = self._generate_a_non_existent_security_group_id()
+        s_name = data_utils.rand_name('sg-')
+        s_description = data_utils.rand_name('description-')
+        self.assertRaises(exceptions.NotFound,
+                          self.client.update_security_group,
+                          non_exist_id, name=s_name,
+                          description=s_description)
+
+
+class SecurityGroupsNegativeTestXML(SecurityGroupsNegativeTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index d985d2b..f195562 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -287,6 +287,7 @@
         self.addCleanup(self.os.image_client.delete_image, image3_id)
         self.assertEqual(202, resp.status)
         # the first back up should be deleted
+        self.servers_client.wait_for_server_status(self.server_id, 'ACTIVE')
         self.os.image_client.wait_for_resource_deletion(image1_id)
         oldest_backup_exist = False
         resp, image_list = self.os.image_client.image_list_detail(
diff --git a/tempest/api/compute/servers/test_server_addresses.py b/tempest/api/compute/servers/test_server_addresses.py
index 7ca8a52..1e55afb 100644
--- a/tempest/api/compute/servers/test_server_addresses.py
+++ b/tempest/api/compute/servers/test_server_addresses.py
@@ -16,34 +16,20 @@
 #    under the License.
 
 from tempest.api.compute import base
-from tempest import exceptions
-from tempest.test import attr
+from tempest import test
 
 
-class ServerAddressesTest(base.BaseV2ComputeTest):
+class ServerAddressesTestJSON(base.BaseV2ComputeTest):
     _interface = 'json'
 
     @classmethod
     def setUpClass(cls):
-        super(ServerAddressesTest, cls).setUpClass()
+        super(ServerAddressesTestJSON, cls).setUpClass()
         cls.client = cls.servers_client
 
         resp, cls.server = cls.create_test_server(wait_until='ACTIVE')
 
-    @attr(type=['negative', 'gate'])
-    def test_list_server_addresses_invalid_server_id(self):
-        # List addresses request should fail if server id not in system
-        self.assertRaises(exceptions.NotFound, self.client.list_addresses,
-                          '999')
-
-    @attr(type=['negative', 'gate'])
-    def test_list_server_addresses_by_network_neg(self):
-        # List addresses by network should fail if network name not valid
-        self.assertRaises(exceptions.NotFound,
-                          self.client.list_addresses_by_network,
-                          self.server['id'], 'invalid')
-
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_list_server_addresses(self):
         # All public and private addresses for
         # a server should be returned
@@ -60,7 +46,7 @@
                 self.assertTrue(address['addr'])
                 self.assertTrue(address['version'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_list_server_addresses_by_network(self):
         # Providing a network type should filter
         # the addresses return by that type
@@ -80,5 +66,5 @@
                 self.assertTrue(any([a for a in addr if a == address]))
 
 
-class ServerAddressesTestXML(ServerAddressesTest):
+class ServerAddressesTestXML(ServerAddressesTestJSON):
     _interface = 'xml'
diff --git a/tempest/api/compute/servers/test_server_addresses_negative.py b/tempest/api/compute/servers/test_server_addresses_negative.py
new file mode 100644
index 0000000..30aa7d1
--- /dev/null
+++ b/tempest/api/compute/servers/test_server_addresses_negative.py
@@ -0,0 +1,48 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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.compute import base
+from tempest import exceptions
+from tempest import test
+
+
+class ServerAddressesNegativeTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(ServerAddressesNegativeTestJSON, cls).setUpClass()
+        cls.client = cls.servers_client
+
+        resp, cls.server = cls.create_test_server(wait_until='ACTIVE')
+
+    @test.attr(type=['negative', 'gate'])
+    def test_list_server_addresses_invalid_server_id(self):
+        # List addresses request should fail if server id not in system
+        self.assertRaises(exceptions.NotFound, self.client.list_addresses,
+                          '999')
+
+    @test.attr(type=['negative', 'gate'])
+    def test_list_server_addresses_by_network_neg(self):
+        # List addresses by network should fail if network name not valid
+        self.assertRaises(exceptions.NotFound,
+                          self.client.list_addresses_by_network,
+                          self.server['id'], 'invalid')
+
+
+class ServerAddressesNegativeTestXML(ServerAddressesNegativeTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/test_auth_token.py b/tempest/api/compute/test_auth_token.py
deleted file mode 100644
index e52c415..0000000
--- a/tempest/api/compute/test_auth_token.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# 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.
-
-
-import testtools
-
-from tempest.api.compute import base
-import tempest.config as config
-
-CONF = config.CONF
-
-
-class AuthTokenTestJSON(base.BaseV2ComputeTest):
-    _interface = 'json'
-
-    @classmethod
-    def setUpClass(cls):
-        super(AuthTokenTestJSON, cls).setUpClass()
-
-        cls.servers_v2 = cls.os.servers_client
-        cls.servers_v3 = cls.os.servers_client_v3_auth
-
-    def test_v2_token(self):
-        # Can get a token using v2 of the identity API and use that to perform
-        # an operation on the compute service.
-
-        # Doesn't matter which compute API is used,
-        # picking list_servers because it's easy.
-        self.servers_v2.list_servers()
-
-    @testtools.skipIf(not CONF.identity.uri_v3,
-                      'v3 auth client not configured')
-    def test_v3_token(self):
-        # Can get a token using v3 of the identity API and use that to perform
-        # an operation on the compute service.
-
-        # Doesn't matter which compute API is used,
-        # picking list_servers because it's easy.
-        self.servers_v3.list_servers()
-
-
-class AuthTokenTestXML(AuthTokenTestJSON):
-    _interface = 'xml'
diff --git a/tempest/api/compute/v3/images/test_image_metadata.py b/tempest/api/compute/v3/images/test_image_metadata.py
new file mode 100644
index 0000000..76e0cae
--- /dev/null
+++ b/tempest/api/compute/v3/images/test_image_metadata.py
@@ -0,0 +1,114 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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.compute import base
+from tempest.common.utils import data_utils
+from tempest.test import attr
+
+
+class ImagesMetadataTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(ImagesMetadataTestJSON, cls).setUpClass()
+        if not cls.config.service_available.glance:
+            skip_msg = ("%s skipped as glance is not available" % cls.__name__)
+            raise cls.skipException(skip_msg)
+
+        cls.servers_client = cls.servers_client
+        cls.client = cls.images_client
+
+        resp, server = cls.create_test_server(wait_until='ACTIVE')
+        cls.server_id = server['id']
+
+        # Snapshot the server once to save time
+        name = data_utils.rand_name('image')
+        resp, _ = cls.client.create_image(cls.server_id, name, {})
+        cls.image_id = resp['location'].rsplit('/', 1)[1]
+
+        cls.client.wait_for_image_status(cls.image_id, 'ACTIVE')
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.client.delete_image(cls.image_id)
+        super(ImagesMetadataTestJSON, cls).tearDownClass()
+
+    def setUp(self):
+        super(ImagesMetadataTestJSON, self).setUp()
+        meta = {'key1': 'value1', 'key2': 'value2'}
+        resp, _ = self.client.set_image_metadata(self.image_id, meta)
+        self.assertEqual(resp.status, 200)
+
+    @attr(type='gate')
+    def test_list_image_metadata(self):
+        # All metadata key/value pairs for an image should be returned
+        resp, resp_metadata = self.client.list_image_metadata(self.image_id)
+        expected = {'key1': 'value1', 'key2': 'value2'}
+        self.assertEqual(expected, resp_metadata)
+
+    @attr(type='gate')
+    def test_set_image_metadata(self):
+        # The metadata for the image should match the new values
+        req_metadata = {'meta2': 'value2', 'meta3': 'value3'}
+        resp, body = self.client.set_image_metadata(self.image_id,
+                                                    req_metadata)
+
+        resp, resp_metadata = self.client.list_image_metadata(self.image_id)
+        self.assertEqual(req_metadata, resp_metadata)
+
+    @attr(type='gate')
+    def test_update_image_metadata(self):
+        # The metadata for the image should match the updated values
+        req_metadata = {'key1': 'alt1', 'key3': 'value3'}
+        resp, metadata = self.client.update_image_metadata(self.image_id,
+                                                           req_metadata)
+
+        resp, resp_metadata = self.client.list_image_metadata(self.image_id)
+        expected = {'key1': 'alt1', 'key2': 'value2', 'key3': 'value3'}
+        self.assertEqual(expected, resp_metadata)
+
+    @attr(type='gate')
+    def test_get_image_metadata_item(self):
+        # The value for a specific metadata key should be returned
+        resp, meta = self.client.get_image_metadata_item(self.image_id,
+                                                         'key2')
+        self.assertEqual('value2', meta['key2'])
+
+    @attr(type='gate')
+    def test_set_image_metadata_item(self):
+        # The value provided for the given meta item should be set for
+        # the image
+        meta = {'key1': 'alt'}
+        resp, body = self.client.set_image_metadata_item(self.image_id,
+                                                         'key1', meta)
+        resp, resp_metadata = self.client.list_image_metadata(self.image_id)
+        expected = {'key1': 'alt', 'key2': 'value2'}
+        self.assertEqual(expected, resp_metadata)
+
+    @attr(type='gate')
+    def test_delete_image_metadata_item(self):
+        # The metadata value/key pair should be deleted from the image
+        resp, body = self.client.delete_image_metadata_item(self.image_id,
+                                                            'key1')
+        resp, resp_metadata = self.client.list_image_metadata(self.image_id)
+        expected = {'key2': 'value2'}
+        self.assertEqual(expected, resp_metadata)
+
+
+class ImagesMetadataTestXML(ImagesMetadataTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/images/test_image_metadata_negative.py b/tempest/api/compute/v3/images/test_image_metadata_negative.py
new file mode 100644
index 0000000..1767e5d
--- /dev/null
+++ b/tempest/api/compute/v3/images/test_image_metadata_negative.py
@@ -0,0 +1,81 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack Foundation
+# 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.compute import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class ImagesMetadataTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(ImagesMetadataTestJSON, cls).setUpClass()
+        cls.client = cls.images_client
+
+    @attr(type=['negative', 'gate'])
+    def test_list_nonexistent_image_metadata(self):
+        # Negative test: List on nonexistent image
+        # metadata should not happen
+        self.assertRaises(exceptions.NotFound, self.client.list_image_metadata,
+                          data_utils.rand_uuid())
+
+    @attr(type=['negative', 'gate'])
+    def test_update_nonexistent_image_metadata(self):
+        # Negative test:An update should not happen for a non-existent image
+        meta = {'key1': 'alt1', 'key2': 'alt2'}
+        self.assertRaises(exceptions.NotFound,
+                          self.client.update_image_metadata,
+                          data_utils.rand_uuid(), meta)
+
+    @attr(type=['negative', 'gate'])
+    def test_get_nonexistent_image_metadata_item(self):
+        # Negative test: Get on non-existent image should not happen
+        self.assertRaises(exceptions.NotFound,
+                          self.client.get_image_metadata_item,
+                          data_utils.rand_uuid(), 'key2')
+
+    @attr(type=['negative', 'gate'])
+    def test_set_nonexistent_image_metadata(self):
+        # Negative test: Metadata should not be set to a non-existent image
+        meta = {'key1': 'alt1', 'key2': 'alt2'}
+        self.assertRaises(exceptions.NotFound, self.client.set_image_metadata,
+                          data_utils.rand_uuid(), meta)
+
+    @attr(type=['negative', 'gate'])
+    def test_set_nonexistent_image_metadata_item(self):
+        # Negative test: Metadata item should not be set to a
+        # nonexistent image
+        meta = {'key1': 'alt'}
+        self.assertRaises(exceptions.NotFound,
+                          self.client.set_image_metadata_item,
+                          data_utils.rand_uuid(), 'key1',
+                          meta)
+
+    @attr(type=['negative', 'gate'])
+    def test_delete_nonexistent_image_metadata_item(self):
+        # Negative test: Shouldn't be able to delete metadata
+        # item from non-existent image
+        self.assertRaises(exceptions.NotFound,
+                          self.client.delete_image_metadata_item,
+                          data_utils.rand_uuid(), 'key1')
+
+
+class ImagesMetadataTestXML(ImagesMetadataTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/images/test_images_oneserver.py b/tempest/api/compute/v3/images/test_images_oneserver.py
new file mode 100644
index 0000000..26cc3f6
--- /dev/null
+++ b/tempest/api/compute/v3/images/test_images_oneserver.py
@@ -0,0 +1,138 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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 testtools
+
+from tempest.api.compute import base
+from tempest import clients
+from tempest.common.utils import data_utils
+from tempest import config
+from tempest.openstack.common import log as logging
+from tempest.test import attr
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class ImagesOneServerTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    def tearDown(self):
+        """Terminate test instances created after a test is executed."""
+        for image_id in self.image_ids:
+            self.client.delete_image(image_id)
+            self.image_ids.remove(image_id)
+        super(ImagesOneServerTestJSON, self).tearDown()
+
+    def setUp(self):
+        # NOTE(afazekas): Normally we use the same server with all test cases,
+        # but if it has an issue, we build a new one
+        super(ImagesOneServerTestJSON, self).setUp()
+        # Check if the server is in a clean state after test
+        try:
+            self.servers_client.wait_for_server_status(self.server_id,
+                                                       'ACTIVE')
+        except Exception:
+            LOG.exception('server %s timed out to become ACTIVE. rebuilding'
+                          % self.server_id)
+            # Rebuild server if cannot reach the ACTIVE state
+            # Usually it means the server had a serious accident
+            self.__class__.server_id = self.rebuild_server(self.server_id)
+
+    @classmethod
+    def setUpClass(cls):
+        super(ImagesOneServerTestJSON, cls).setUpClass()
+        cls.client = cls.images_client
+        if not cls.config.service_available.glance:
+            skip_msg = ("%s skipped as glance is not available" % cls.__name__)
+            raise cls.skipException(skip_msg)
+
+        try:
+            resp, server = cls.create_test_server(wait_until='ACTIVE')
+            cls.server_id = server['id']
+        except Exception:
+            cls.tearDownClass()
+            raise
+
+        cls.image_ids = []
+
+        if cls.multi_user:
+            if cls.config.compute.allow_tenant_isolation:
+                creds = cls.isolated_creds.get_alt_creds()
+                username, tenant_name, password = creds
+                cls.alt_manager = clients.Manager(username=username,
+                                                  password=password,
+                                                  tenant_name=tenant_name)
+            else:
+                # Use the alt_XXX credentials in the config file
+                cls.alt_manager = clients.AltManager()
+            cls.alt_client = cls.alt_manager.images_client
+
+    def _get_default_flavor_disk_size(self, flavor_id):
+        resp, flavor = self.flavors_client.get_flavor_details(flavor_id)
+        return flavor['disk']
+
+    @testtools.skipUnless(CONF.compute_feature_enabled.create_image,
+                          'Environment unable to create images.')
+    @attr(type='smoke')
+    def test_create_delete_image(self):
+
+        # Create a new image
+        name = data_utils.rand_name('image')
+        meta = {'image_type': 'test'}
+        resp, body = self.client.create_image(self.server_id, name, meta)
+        self.assertEqual(202, resp.status)
+        image_id = data_utils.parse_image_id(resp['location'])
+        self.client.wait_for_image_status(image_id, 'ACTIVE')
+
+        # Verify the image was created correctly
+        resp, image = self.client.get_image(image_id)
+        self.assertEqual(name, image['name'])
+        self.assertEqual('test', image['metadata']['image_type'])
+
+        resp, original_image = self.client.get_image(self.image_ref)
+
+        # Verify minRAM is the same as the original image
+        self.assertEqual(image['minRam'], original_image['minRam'])
+
+        # Verify minDisk is the same as the original image or the flavor size
+        flavor_disk_size = self._get_default_flavor_disk_size(self.flavor_ref)
+        self.assertIn(str(image['minDisk']),
+                      (str(original_image['minDisk']), str(flavor_disk_size)))
+
+        # Verify the image was deleted correctly
+        resp, body = self.client.delete_image(image_id)
+        self.assertEqual('204', resp['status'])
+        self.client.wait_for_resource_deletion(image_id)
+
+    @attr(type=['gate'])
+    def test_create_image_specify_multibyte_character_image_name(self):
+        if self.__class__._interface == "xml":
+            # NOTE(sdague): not entirely accurage, but we'd need a ton of work
+            # in our XML client to make this good
+            raise self.skipException("Not testable in XML")
+        # prefix character is:
+        # http://www.fileformat.info/info/unicode/char/1F4A9/index.htm
+        utf8_name = data_utils.rand_name(u'\xF0\x9F\x92\xA9')
+        resp, body = self.client.create_image(self.server_id, utf8_name)
+        image_id = data_utils.parse_image_id(resp['location'])
+        self.addCleanup(self.client.delete_image, image_id)
+        self.assertEqual('202', resp['status'])
+
+
+class ImagesOneServerTestXML(ImagesOneServerTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/images/test_images_oneserver_negative.py b/tempest/api/compute/v3/images/test_images_oneserver_negative.py
new file mode 100644
index 0000000..5e235d1
--- /dev/null
+++ b/tempest/api/compute/v3/images/test_images_oneserver_negative.py
@@ -0,0 +1,162 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# Copyright 2013 IBM Corp.
+# 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.compute import base
+from tempest import clients
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.openstack.common import log as logging
+from tempest.test import attr
+from tempest.test import skip_because
+
+LOG = logging.getLogger(__name__)
+
+
+class ImagesOneServerNegativeTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    def tearDown(self):
+        """Terminate test instances created after a test is executed."""
+        for image_id in self.image_ids:
+            self.client.delete_image(image_id)
+            self.image_ids.remove(image_id)
+        super(ImagesOneServerNegativeTestJSON, self).tearDown()
+
+    def setUp(self):
+        # NOTE(afazekas): Normally we use the same server with all test cases,
+        # but if it has an issue, we build a new one
+        super(ImagesOneServerNegativeTestJSON, self).setUp()
+        # Check if the server is in a clean state after test
+        try:
+            self.servers_client.wait_for_server_status(self.server_id,
+                                                       'ACTIVE')
+        except Exception:
+            LOG.exception('server %s timed out to become ACTIVE. rebuilding'
+                          % self.server_id)
+            # Rebuild server if cannot reach the ACTIVE state
+            # Usually it means the server had a serious accident
+            self._reset_server()
+
+    def _reset_server(self):
+        self.__class__.server_id = self.rebuild_server(self.server_id)
+
+    @classmethod
+    def setUpClass(cls):
+        super(ImagesOneServerNegativeTestJSON, cls).setUpClass()
+        cls.client = cls.images_client
+        if not cls.config.service_available.glance:
+            skip_msg = ("%s skipped as glance is not available" % cls.__name__)
+            raise cls.skipException(skip_msg)
+
+        try:
+            resp, server = cls.create_test_server(wait_until='ACTIVE')
+            cls.server_id = server['id']
+        except Exception:
+            cls.tearDownClass()
+            raise
+
+        cls.image_ids = []
+
+        if cls.multi_user:
+            if cls.config.compute.allow_tenant_isolation:
+                creds = cls.isolated_creds.get_alt_creds()
+                username, tenant_name, password = creds
+                cls.alt_manager = clients.Manager(username=username,
+                                                  password=password,
+                                                  tenant_name=tenant_name)
+            else:
+                # Use the alt_XXX credentials in the config file
+                cls.alt_manager = clients.AltManager()
+            cls.alt_client = cls.alt_manager.images_client
+
+    @skip_because(bug="1006725")
+    @attr(type=['negative', 'gate'])
+    def test_create_image_specify_multibyte_character_image_name(self):
+        if self.__class__._interface == "xml":
+            raise self.skipException("Not testable in XML")
+        # invalid multibyte sequence from:
+        # http://stackoverflow.com/questions/1301402/
+        #     example-invalid-utf8-string
+        invalid_name = data_utils.rand_name(u'\xc3\x28')
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_image, self.server_id,
+                          invalid_name)
+
+    @attr(type=['negative', 'gate'])
+    def test_create_image_specify_invalid_metadata(self):
+        # Return an error when creating image with invalid metadata
+        snapshot_name = data_utils.rand_name('test-snap-')
+        meta = {'': ''}
+        self.assertRaises(exceptions.BadRequest, self.client.create_image,
+                          self.server_id, snapshot_name, meta)
+
+    @attr(type=['negative', 'gate'])
+    def test_create_image_specify_metadata_over_limits(self):
+        # Return an error when creating image with meta data over 256 chars
+        snapshot_name = data_utils.rand_name('test-snap-')
+        meta = {'a' * 260: 'b' * 260}
+        self.assertRaises(exceptions.BadRequest, self.client.create_image,
+                          self.server_id, snapshot_name, meta)
+
+    @attr(type=['negative', 'gate'])
+    def test_create_second_image_when_first_image_is_being_saved(self):
+        # Disallow creating another image when first image is being saved
+
+        # Create first snapshot
+        snapshot_name = data_utils.rand_name('test-snap-')
+        resp, body = self.client.create_image(self.server_id,
+                                              snapshot_name)
+        self.assertEqual(202, resp.status)
+        image_id = data_utils.parse_image_id(resp['location'])
+        self.image_ids.append(image_id)
+        self.addCleanup(self._reset_server)
+
+        # Create second snapshot
+        alt_snapshot_name = data_utils.rand_name('test-snap-')
+        self.assertRaises(exceptions.Conflict, self.client.create_image,
+                          self.server_id, alt_snapshot_name)
+
+    @attr(type=['negative', 'gate'])
+    def test_create_image_specify_name_over_256_chars(self):
+        # Return an error if snapshot name over 256 characters is passed
+
+        snapshot_name = data_utils.rand_name('a' * 260)
+        self.assertRaises(exceptions.BadRequest, self.client.create_image,
+                          self.server_id, snapshot_name)
+
+    @attr(type=['negative', 'gate'])
+    def test_delete_image_that_is_not_yet_active(self):
+        # Return an error while trying to delete an image what is creating
+
+        snapshot_name = data_utils.rand_name('test-snap-')
+        resp, body = self.client.create_image(self.server_id, snapshot_name)
+        self.assertEqual(202, resp.status)
+        image_id = data_utils.parse_image_id(resp['location'])
+        self.image_ids.append(image_id)
+        self.addCleanup(self._reset_server)
+
+        # Do not wait, attempt to delete the image, ensure it's successful
+        resp, body = self.client.delete_image(image_id)
+        self.assertEqual('204', resp['status'])
+        self.image_ids.remove(image_id)
+
+        self.assertRaises(exceptions.NotFound, self.client.get_image, image_id)
+
+
+class ImagesOneServerNegativeTestXML(ImagesOneServerNegativeTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/images/test_list_image_filters.py b/tempest/api/compute/v3/images/test_list_image_filters.py
new file mode 100644
index 0000000..bfdd8b2
--- /dev/null
+++ b/tempest/api/compute/v3/images/test_list_image_filters.py
@@ -0,0 +1,231 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# 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.compute import base
+from tempest import exceptions
+from tempest.openstack.common import log as logging
+from tempest.test import attr
+
+
+LOG = logging.getLogger(__name__)
+
+
+class ListImageFiltersTestJSON(base.BaseV2ComputeTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(ListImageFiltersTestJSON, cls).setUpClass()
+        if not cls.config.service_available.glance:
+            skip_msg = ("%s skipped as glance is not available" % cls.__name__)
+            raise cls.skipException(skip_msg)
+        cls.client = cls.images_client
+        cls.image_ids = []
+
+        try:
+            resp, cls.server1 = cls.create_test_server()
+            resp, cls.server2 = cls.create_test_server(wait_until='ACTIVE')
+            # NOTE(sdague) this is faster than doing the sync wait_util on both
+            cls.servers_client.wait_for_server_status(cls.server1['id'],
+                                                      'ACTIVE')
+
+            # Create images to be used in the filter tests
+            resp, cls.image1 = cls.create_image_from_server(
+                cls.server1['id'], wait_until='ACTIVE')
+            cls.image1_id = cls.image1['id']
+
+            # Servers have a hidden property for when they are being imaged
+            # Performing back-to-back create image calls on a single
+            # server will sometimes cause failures
+            resp, cls.image3 = cls.create_image_from_server(
+                cls.server2['id'], wait_until='ACTIVE')
+            cls.image3_id = cls.image3['id']
+
+            # Wait for the server to be active after the image upload
+            resp, cls.image2 = cls.create_image_from_server(
+                cls.server1['id'], wait_until='ACTIVE')
+            cls.image2_id = cls.image2['id']
+        except Exception:
+            LOG.exception('setUpClass failed')
+            cls.tearDownClass()
+            raise
+
+    @attr(type=['negative', 'gate'])
+    def test_get_image_not_existing(self):
+        # Check raises a NotFound
+        self.assertRaises(exceptions.NotFound, self.client.get_image,
+                          "nonexistingimageid")
+
+    @attr(type='gate')
+    def test_list_images_filter_by_status(self):
+        # The list of images should contain only images with the
+        # provided status
+        params = {'status': 'ACTIVE'}
+        resp, images = self.client.list_images(params)
+
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image3_id]))
+
+    @attr(type='gate')
+    def test_list_images_filter_by_name(self):
+        # List of all images should contain the expected images filtered
+        # by name
+        params = {'name': self.image1['name']}
+        resp, images = self.client.list_images(params)
+
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+        self.assertFalse(any([i for i in images if i['id'] == self.image2_id]))
+        self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
+
+    @attr(type='gate')
+    def test_list_images_filter_by_server_id(self):
+        # The images should contain images filtered by server id
+        params = {'server': self.server1['id']}
+        resp, images = self.client.list_images(params)
+
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]),
+                        "Failed to find image %s in images. Got images %s" %
+                        (self.image1_id, images))
+        self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
+        self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
+
+    @attr(type='gate')
+    def test_list_images_filter_by_server_ref(self):
+        # The list of servers should be filtered by server ref
+        server_links = self.server2['links']
+
+        # Try all server link types
+        for link in server_links:
+            params = {'server': link['href']}
+            resp, images = self.client.list_images(params)
+
+            self.assertFalse(any([i for i in images
+                                  if i['id'] == self.image1_id]))
+            self.assertFalse(any([i for i in images
+                                  if i['id'] == self.image2_id]))
+            self.assertTrue(any([i for i in images
+                                 if i['id'] == self.image3_id]))
+
+    @attr(type='gate')
+    def test_list_images_filter_by_type(self):
+        # The list of servers should be filtered by image type
+        params = {'type': 'snapshot'}
+        resp, images = self.client.list_images(params)
+
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image3_id]))
+        self.assertFalse(any([i for i in images if i['id'] == self.image_ref]))
+
+    @attr(type='gate')
+    def test_list_images_limit_results(self):
+        # Verify only the expected number of results are returned
+        params = {'limit': '1'}
+        resp, images = self.client.list_images(params)
+        # when _interface='xml', one element for images_links in images
+        # ref: Question #224349
+        self.assertEqual(1, len([x for x in images if 'id' in x]))
+
+    @attr(type='gate')
+    def test_list_images_filter_by_changes_since(self):
+        # Verify only updated images are returned in the detailed list
+
+        # Becoming ACTIVE will modify the updated time
+        # Filter by the image's created time
+        params = {'changes-since': self.image3['created']}
+        resp, images = self.client.list_images(params)
+        found = any([i for i in images if i['id'] == self.image3_id])
+        self.assertTrue(found)
+
+    @attr(type='gate')
+    def test_list_images_with_detail_filter_by_status(self):
+        # Detailed list of all images should only contain images
+        # with the provided status
+        params = {'status': 'ACTIVE'}
+        resp, images = self.client.list_images_with_detail(params)
+
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image3_id]))
+
+    @attr(type='gate')
+    def test_list_images_with_detail_filter_by_name(self):
+        # Detailed list of all images should contain the expected
+        # images filtered by name
+        params = {'name': self.image1['name']}
+        resp, images = self.client.list_images_with_detail(params)
+
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+        self.assertFalse(any([i for i in images if i['id'] == self.image2_id]))
+        self.assertFalse(any([i for i in images if i['id'] == self.image3_id]))
+
+    @attr(type='gate')
+    def test_list_images_with_detail_limit_results(self):
+        # Verify only the expected number of results (with full details)
+        # are returned
+        params = {'limit': '1'}
+        resp, images = self.client.list_images_with_detail(params)
+        self.assertEqual(1, len(images))
+
+    @attr(type='gate')
+    def test_list_images_with_detail_filter_by_server_ref(self):
+        # Detailed list of servers should be filtered by server ref
+        server_links = self.server2['links']
+
+        # Try all server link types
+        for link in server_links:
+            params = {'server': link['href']}
+            resp, images = self.client.list_images_with_detail(params)
+
+            self.assertFalse(any([i for i in images
+                                  if i['id'] == self.image1_id]))
+            self.assertFalse(any([i for i in images
+                                  if i['id'] == self.image2_id]))
+            self.assertTrue(any([i for i in images
+                                 if i['id'] == self.image3_id]))
+
+    @attr(type='gate')
+    def test_list_images_with_detail_filter_by_type(self):
+        # The detailed list of servers should be filtered by image type
+        params = {'type': 'snapshot'}
+        resp, images = self.client.list_images_with_detail(params)
+        resp, image4 = self.client.get_image(self.image_ref)
+
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image2_id]))
+        self.assertTrue(any([i for i in images if i['id'] == self.image3_id]))
+        self.assertFalse(any([i for i in images if i['id'] == self.image_ref]))
+
+    @attr(type='gate')
+    def test_list_images_with_detail_filter_by_changes_since(self):
+        # Verify an update image is returned
+
+        # Becoming ACTIVE will modify the updated time
+        # Filter by the image's created time
+        params = {'changes-since': self.image1['created']}
+        resp, images = self.client.list_images_with_detail(params)
+        self.assertTrue(any([i for i in images if i['id'] == self.image1_id]))
+
+    @attr(type=['negative', 'gate'])
+    def test_get_nonexistant_image(self):
+        # Negative test: GET on non-existent image should fail
+        self.assertRaises(exceptions.NotFound, self.client.get_image, 999)
+
+
+class ListImageFiltersTestXML(ListImageFiltersTestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/identity/admin/v3/test_trusts.py b/tempest/api/identity/admin/v3/test_trusts.py
index e1d28d7..1ecc90c 100644
--- a/tempest/api/identity/admin/v3/test_trusts.py
+++ b/tempest/api/identity/admin/v3/test_trusts.py
@@ -31,7 +31,14 @@
 
         self.trust_id = None
         self.create_trustor_and_roles()
-        self.addCleanup(self.cleanup_trust_user_and_roles)
+        self.addCleanup(self.cleanup_user_and_roles)
+
+    def tearDown(self):
+        if self.trust_id:
+            # Do the delete in tearDown not addCleanup - we want the test to
+            # fail in the event there is a bug which causes undeletable trusts
+            self.delete_trust()
+        super(BaseTrustsV3Test, self).tearDown()
 
     def create_trustor_and_roles(self):
         # Get trustor project ID, use the admin project
@@ -86,14 +93,7 @@
                              interface=self._interface)
         self.trustor_v3_client = os.identity_v3_client
 
-    def cleanup_trust_user_and_roles(self):
-        if self.trust_id:
-            try:
-                self.trustor_v3_client.delete_trust(self.trust_id)
-            except exceptions.NotFound:
-                pass
-            self.trust_id = None
-
+    def cleanup_user_and_roles(self):
         if self.trustor_user_id:
             self.v3_client.delete_user(self.trustor_user_id)
         if self.delegated_role_id:
@@ -206,8 +206,6 @@
 
         self.check_trust_roles()
 
-        self.delete_trust()
-
     @attr(type='smoke')
     def test_trust_noimpersonate(self):
         # Test case to check we can create, get and delete a trust
@@ -220,8 +218,6 @@
 
         self.check_trust_roles()
 
-        self.delete_trust()
-
     @attr(type='smoke')
     def test_trust_expire(self):
         # Test case to check we can create, get and delete a trust
@@ -238,8 +234,6 @@
 
         self.check_trust_roles()
 
-        self.delete_trust()
-
     @attr(type='smoke')
     def test_trust_expire_invalid(self):
         # Test case to check we can check an invlaid expiry time
diff --git a/tempest/api/network/base_routers.py b/tempest/api/network/base_routers.py
new file mode 100644
index 0000000..9baef04
--- /dev/null
+++ b/tempest/api/network/base_routers.py
@@ -0,0 +1,50 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack Foundation
+# 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.network import base
+
+
+class BaseRouterTest(base.BaseAdminNetworkTest):
+    # NOTE(salv-orlando): This class inherits from BaseAdminNetworkTest
+    # as some router operations, such as enabling or disabling SNAT
+    # require admin credentials by default
+
+    @classmethod
+    def setUpClass(cls):
+        super(BaseRouterTest, cls).setUpClass()
+
+    def _delete_router(self, router_id):
+        resp, _ = self.client.delete_router(router_id)
+        self.assertEqual(204, resp.status)
+        # Asserting that the router is not found in the list
+        # after deletion
+        resp, list_body = self.client.list_routers()
+        self.assertEqual('200', resp['status'])
+        routers_list = list()
+        for router in list_body['routers']:
+            routers_list.append(router['id'])
+        self.assertNotIn(router_id, routers_list)
+
+    def _remove_router_interface_with_subnet_id(self, router_id, subnet_id):
+        resp, _ = self.client.remove_router_interface_with_subnet_id(
+            router_id, subnet_id)
+        self.assertEqual('200', resp['status'])
+
+    def _remove_router_interface_with_port_id(self, router_id, port_id):
+        resp, _ = self.client.remove_router_interface_with_port_id(
+            router_id, port_id)
+        self.assertEqual('200', resp['status'])
diff --git a/tempest/api/network/test_load_balancer.py b/tempest/api/network/test_load_balancer.py
index 224c36c..d1ad134 100644
--- a/tempest/api/network/test_load_balancer.py
+++ b/tempest/api/network/test_load_balancer.py
@@ -17,7 +17,7 @@
 
 from tempest.api.network import base
 from tempest.common.utils import data_utils
-from tempest.test import attr
+from tempest import test
 
 
 class LoadBalancerJSON(base.BaseNetworkTest):
@@ -42,6 +42,9 @@
     @classmethod
     def setUpClass(cls):
         super(LoadBalancerJSON, cls).setUpClass()
+        if not test.is_extension_enabled('lbaas', 'network'):
+            msg = "lbaas extension not enabled."
+            raise cls.skipException(msg)
         cls.network = cls.create_network()
         cls.name = cls.network['name']
         cls.subnet = cls.create_subnet(cls.network)
@@ -53,7 +56,7 @@
         cls.member = cls.create_member(80, cls.pool)
         cls.health_monitor = cls.create_health_monitor(4, 3, "TCP", 1)
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_list_vips(self):
         # Verify the vIP exists in the list of all vIPs
         resp, body = self.client.list_vips()
@@ -100,7 +103,7 @@
         resp, body = self.client.delete_pool(pool['id'])
         self.assertEqual('204', resp['status'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_show_vip(self):
         # Verifies the details of a vip
         resp, body = self.client.show_vip(self.vip['id'])
@@ -109,7 +112,7 @@
         self.assertEqual(self.vip['id'], vip['id'])
         self.assertEqual(self.vip['name'], vip['name'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_show_pool(self):
         # Verifies the details of a pool
         resp, body = self.client.show_pool(self.pool['id'])
@@ -118,7 +121,7 @@
         self.assertEqual(self.pool['id'], pool['id'])
         self.assertEqual(self.pool['name'], pool['name'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_list_pools(self):
         # Verify the pool exists in the list of all pools
         resp, body = self.client.list_pools()
@@ -126,7 +129,7 @@
         pools = body['pools']
         self.assertIn(self.pool['id'], [p['id'] for p in pools])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_list_members(self):
         # Verify the member exists in the list of all members
         resp, body = self.client.list_members()
@@ -134,7 +137,7 @@
         members = body['members']
         self.assertIn(self.member['id'], [m['id'] for m in members])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_create_update_delete_member(self):
         # Creates a member
         resp, body = self.client.create_member("10.0.9.47", 80,
@@ -151,7 +154,7 @@
         resp, body = self.client.delete_member(member['id'])
         self.assertEqual('204', resp['status'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_show_member(self):
         # Verifies the details of a member
         resp, body = self.client.show_member(self.member['id'])
@@ -161,7 +164,7 @@
         self.assertEqual(self.member['admin_state_up'],
                          member['admin_state_up'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_list_health_monitors(self):
         # Verify the health monitor exists in the list of all health monitors
         resp, body = self.client.list_health_monitors()
@@ -170,7 +173,7 @@
         self.assertIn(self.health_monitor['id'],
                       [h['id'] for h in health_monitors])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_create_update_delete_health_monitor(self):
         # Creates a health_monitor
         resp, body = self.client.create_health_monitor(4, 3, "TCP", 1)
@@ -187,7 +190,7 @@
         resp, body = self.client.delete_health_monitor(health_monitor['id'])
         self.assertEqual('204', resp['status'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_show_health_monitor(self):
         # Verifies the details of a health_monitor
         resp, body = self.client.show_health_monitor(self.health_monitor['id'])
@@ -197,7 +200,7 @@
         self.assertEqual(self.health_monitor['admin_state_up'],
                          health_monitor['admin_state_up'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_associate_disassociate_health_monitor_with_pool(self):
         # Verify that a health monitor can be associated with a pool
         resp, body = (self.client.associate_health_monitor_with_pool
diff --git a/tempest/api/network/test_routers.py b/tempest/api/network/test_routers.py
index 21934f2..b6022e8 100644
--- a/tempest/api/network/test_routers.py
+++ b/tempest/api/network/test_routers.py
@@ -17,43 +17,18 @@
 
 import netaddr
 
-from tempest.api.network import base
+from tempest.api.network import base_routers as base
 from tempest.common.utils import data_utils
 from tempest import test
 
 
-class RoutersTest(base.BaseAdminNetworkTest):
-    # NOTE(salv-orlando): This class inherits from BaseAdminNetworkTest
-    # as some router operations, such as enabling or disabling SNAT
-    # require admin credentials by default
+class RoutersTest(base.BaseRouterTest):
     _interface = 'json'
 
     @classmethod
     def setUpClass(cls):
         super(RoutersTest, cls).setUpClass()
 
-    def _delete_router(self, router_id):
-        resp, _ = self.client.delete_router(router_id)
-        self.assertEqual(204, resp.status)
-        # Asserting that the router is not found in the list
-        # after deletion
-        resp, list_body = self.client.list_routers()
-        self.assertEqual('200', resp['status'])
-        routers_list = list()
-        for router in list_body['routers']:
-            routers_list.append(router['id'])
-        self.assertNotIn(router_id, routers_list)
-
-    def _remove_router_interface_with_subnet_id(self, router_id, subnet_id):
-        resp, _ = self.client.remove_router_interface_with_subnet_id(
-            router_id, subnet_id)
-        self.assertEqual('200', resp['status'])
-
-    def _remove_router_interface_with_port_id(self, router_id, port_id):
-        resp, _ = self.client.remove_router_interface_with_port_id(
-            router_id, port_id)
-        self.assertEqual('200', resp['status'])
-
     @test.attr(type='smoke')
     def test_create_show_list_update_delete_router(self):
         # Create a router
diff --git a/tempest/api/network/test_routers_negative.py b/tempest/api/network/test_routers_negative.py
new file mode 100644
index 0000000..520a612
--- /dev/null
+++ b/tempest/api/network/test_routers_negative.py
@@ -0,0 +1,57 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack Foundation
+# 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.network import base_routers as base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class RoutersNegativeTest(base.BaseRouterTest):
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(RoutersNegativeTest, cls).setUpClass()
+        cls.router = cls.create_router(data_utils.rand_name('router-'))
+        cls.network = cls.create_network()
+        cls.subnet = cls.create_subnet(cls.network)
+
+    @attr(type=['negative', 'smoke'])
+    def test_router_add_gateway_invalid_network_returns_404(self):
+        self.assertRaises(exceptions.NotFound,
+                          self.client.update_router,
+                          self.router['id'],
+                          external_gateway_info={
+                              'network_id': self.router['id']})
+
+    @attr(type=['negative', 'smoke'])
+    def test_router_add_gateway_net_not_external_returns_400(self):
+        self.create_subnet(self.network)
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_router,
+                          self.router['id'],
+                          external_gateway_info={
+                              'network_id': self.network['id']})
+
+    @attr(type=['negative', 'smoke'])
+    def test_router_remove_interface_in_use_returns_409(self):
+        self.client.add_router_interface_with_subnet_id(
+            self.router['id'], self.subnet['id'])
+        self.assertRaises(exceptions.Conflict,
+                          self.client.delete_router,
+                          self.router['id'])
diff --git a/tempest/api/network/test_vpnaas_extensions.py b/tempest/api/network/test_vpnaas_extensions.py
index fc3b1d9..d196886 100644
--- a/tempest/api/network/test_vpnaas_extensions.py
+++ b/tempest/api/network/test_vpnaas_extensions.py
@@ -17,7 +17,7 @@
 
 from tempest.api.network import base
 from tempest.common.utils import data_utils
-from tempest.test import attr
+from tempest import test
 
 
 class VPNaaSJSON(base.BaseNetworkTest):
@@ -38,6 +38,9 @@
     @classmethod
     def setUpClass(cls):
         super(VPNaaSJSON, cls).setUpClass()
+        if not test.is_extension_enabled('vpnaas', 'network'):
+            msg = "vpnaas extension not enabled."
+            raise cls.skipException(msg)
         cls.network = cls.create_network()
         cls.subnet = cls.create_subnet(cls.network)
         cls.router = cls.create_router(
@@ -65,7 +68,7 @@
                 ike_id_list.append(i['id'])
             self.assertNotIn(ike_policy_id, ike_id_list)
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_list_vpn_services(self):
         # Verify the VPN service exists in the list of all VPN services
         resp, body = self.client.list_vpnservices()
@@ -73,7 +76,7 @@
         vpnservices = body['vpnservices']
         self.assertIn(self.vpnservice['id'], [v['id'] for v in vpnservices])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_create_update_delete_vpn_service(self):
         # Creates a VPN service
         name = data_utils.rand_name('vpn-service-')
@@ -102,7 +105,7 @@
         vpn_services = [vs['id'] for vs in body['vpnservices']]
         self.assertNotIn(vpnservice['id'], vpn_services)
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_show_vpn_service(self):
         # Verifies the details of a vpn service
         resp, body = self.client.show_vpnservice(self.vpnservice['id'])
@@ -116,7 +119,7 @@
         self.assertEqual(self.vpnservice['subnet_id'], vpnservice['subnet_id'])
         self.assertEqual(self.vpnservice['tenant_id'], vpnservice['tenant_id'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_list_ike_policies(self):
         # Verify the ike policy exists in the list of all IKE policies
         resp, body = self.client.list_ikepolicies()
@@ -124,7 +127,7 @@
         ikepolicies = body['ikepolicies']
         self.assertIn(self.ikepolicy['id'], [i['id'] for i in ikepolicies])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_create_update_delete_ike_policy(self):
         # Creates a IKE policy
         name = data_utils.rand_name('ike-policy-')
@@ -149,7 +152,7 @@
         resp, body = self.client.delete_ikepolicy(ikepolicy['id'])
         self.assertEqual('204', resp['status'])
 
-    @attr(type='smoke')
+    @test.attr(type='smoke')
     def test_show_ike_policy(self):
         # Verifies the details of a ike policy
         resp, body = self.client.show_ikepolicy(self.ikepolicy['id'])
diff --git a/tempest/api/telemetry/__init__.py b/tempest/api/telemetry/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api/telemetry/__init__.py
diff --git a/tempest/api/telemetry/base.py b/tempest/api/telemetry/base.py
new file mode 100644
index 0000000..1f661a6
--- /dev/null
+++ b/tempest/api/telemetry/base.py
@@ -0,0 +1,27 @@
+#    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 import config
+import tempest.test
+
+CONF = config.CONF
+
+
+class BaseTelemetryTest(tempest.test.BaseTestCase):
+
+    """Base test case class for all Telemetry API tests."""
+
+    @classmethod
+    def setUpClass(cls):
+        super(BaseTelemetryTest, cls).setUpClass()
+        if not CONF.service_available.ceilometer:
+            raise cls.skipException("Ceilometer support is required")
diff --git a/tempest/api/volume/test_snapshot_metadata.py b/tempest/api/volume/test_snapshot_metadata.py
new file mode 100644
index 0000000..0326f3c
--- /dev/null
+++ b/tempest/api/volume/test_snapshot_metadata.py
@@ -0,0 +1,119 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Huawei Technologies Co.,LTD
+# 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.volume import base
+from tempest import test
+
+
+class SnapshotMetadataTest(base.BaseVolumeV1Test):
+    _interface = "json"
+
+    @classmethod
+    def setUpClass(cls):
+        super(SnapshotMetadataTest, cls).setUpClass()
+        cls.client = cls.snapshots_client
+        # Create a volume
+        cls.volume = cls.create_volume()
+        # Create a snapshot
+        cls.snapshot = cls.create_snapshot(volume_id=cls.volume['id'])
+        cls.snapshot_id = cls.snapshot['id']
+
+    def tearDown(self):
+        # Update the metadata to {}
+        self.client.update_snapshot_metadata(self.snapshot_id, {})
+        super(SnapshotMetadataTest, self).tearDown()
+
+    @test.attr(type='gate')
+    def test_create_get_delete_snapshot_metadata(self):
+        # Create metadata for the snapshot
+        metadata = {"key1": "value1",
+                    "key2": "value2",
+                    "key3": "value3"}
+        expected = {"key2": "value2",
+                    "key3": "value3"}
+        resp, body = self.client.create_snapshot_metadata(self.snapshot_id,
+                                                          metadata)
+        self.assertEqual(200, resp.status)
+        # Get the metadata of the snapshot
+        resp, body = self.client.get_snapshot_metadata(self.snapshot_id)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(metadata, body)
+        # Delete one item metadata of the snapshot
+        resp, body = self.client.delete_snapshot_metadata_item(
+            self.snapshot_id,
+            "key1")
+        self.assertEqual(200, resp.status)
+        resp, body = self.client.get_snapshot_metadata(self.snapshot_id)
+        self.assertEqual(expected, body)
+
+    @test.attr(type='gate')
+    def test_update_snapshot_metadata(self):
+        # Update metadata for the snapshot
+        metadata = {"key1": "value1",
+                    "key2": "value2",
+                    "key3": "value3"}
+        update = {"key3": "value3_update",
+                  "key4": "value4"}
+        # Create metadata for the snapshot
+        resp, body = self.client.create_snapshot_metadata(self.snapshot_id,
+                                                          metadata)
+        self.assertEqual(200, resp.status)
+        # Get the metadata of the snapshot
+        resp, body = self.client.get_snapshot_metadata(self.snapshot_id)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(metadata, body)
+        # Update metadata item
+        resp, body = self.client.update_snapshot_metadata(
+            self.snapshot_id,
+            update)
+        self.assertEqual(200, resp.status)
+        # Get the metadata of the snapshot
+        resp, body = self.client.get_snapshot_metadata(self.snapshot_id)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(update, body)
+
+    @test.attr(type='gate')
+    def test_update_snapshot_metadata_item(self):
+        # Update metadata item for the snapshot
+        metadata = {"key1": "value1",
+                    "key2": "value2",
+                    "key3": "value3"}
+        update_item = {"key3": "value3_update"}
+        expect = {"key1": "value1",
+                  "key2": "value2",
+                  "key3": "value3_update"}
+        # Create metadata for the snapshot
+        resp, body = self.client.create_snapshot_metadata(self.snapshot_id,
+                                                          metadata)
+        self.assertEqual(200, resp.status)
+        # Get the metadata of the snapshot
+        resp, body = self.client.get_snapshot_metadata(self.snapshot_id)
+        self.assertEqual(metadata, body)
+        # Update metadata item
+        resp, body = self.client.update_snapshot_metadata_item(
+            self.snapshot_id,
+            "key3",
+            update_item)
+        self.assertEqual(200, resp.status)
+        # Get the metadata of the snapshot
+        resp, body = self.client.get_snapshot_metadata(self.snapshot_id)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(expect, body)
+
+
+class SnapshotMetadataTestXML(SnapshotMetadataTest):
+    _interface = "xml"
diff --git a/tempest/api/volume/test_volume_metadata.py b/tempest/api/volume/test_volume_metadata.py
new file mode 100644
index 0000000..0909ade
--- /dev/null
+++ b/tempest/api/volume/test_volume_metadata.py
@@ -0,0 +1,124 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Huawei Technologies Co.,LTD
+# 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.volume.base import BaseVolumeTest
+from tempest import test
+
+
+class VolumeMetadataTest(BaseVolumeTest):
+    _interface = "json"
+
+    @classmethod
+    def setUpClass(cls):
+        super(VolumeMetadataTest, cls).setUpClass()
+        # Create a volume
+        cls.volume = cls.create_volume()
+        cls.volume_id = cls.volume['id']
+
+    @classmethod
+    def tearDownClass(cls):
+        super(VolumeMetadataTest, cls).tearDownClass()
+
+    def tearDown(self):
+        # Update the metadata to {}
+        self.volumes_client.update_volume_metadata(self.volume_id, {})
+        super(VolumeMetadataTest, self).tearDown()
+
+    @test.attr(type='gate')
+    def test_create_get_delete_volume_metadata(self):
+        # Create metadata for the volume
+        metadata = {"key1": "value1",
+                    "key2": "value2",
+                    "key3": "value3"}
+
+        rsp, body = self.volumes_client.create_volume_metadata(self.volume_id,
+                                                               metadata)
+        self.assertEqual(200, rsp.status)
+        # Get the metadata of the volume
+        resp, body = self.volumes_client.get_volume_metadata(self.volume_id)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(metadata, body)
+        # Delete one item metadata of the volume
+        rsp, body = self.volumes_client.delete_volume_metadata_item(
+            self.volume_id,
+            "key1")
+        self.assertEqual(200, rsp.status)
+        resp, body = self.volumes_client.get_volume_metadata(self.volume_id)
+        self.assertNotIn("key1", body)
+
+    @test.attr(type='gate')
+    def test_update_volume_metadata(self):
+        # Update metadata for the volume
+        metadata = {"key1": "value1",
+                    "key2": "value2",
+                    "key3": "value3"}
+
+        update = {"key4": "value4",
+                  "key1": "value1_update"}
+
+        # Create metadata for the volume
+        resp, body = self.volumes_client.create_volume_metadata(
+            self.volume_id,
+            metadata)
+        self.assertEqual(200, resp.status)
+        # Get the metadata of the volume
+        resp, body = self.volumes_client.get_volume_metadata(self.volume_id)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(metadata, body)
+        # Update metadata
+        resp, body = self.volumes_client.update_volume_metadata(
+            self.volume_id,
+            update)
+        self.assertEqual(200, resp.status)
+        # Get the metadata of the volume
+        resp, body = self.volumes_client.get_volume_metadata(self.volume_id)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(update, body)
+
+    @test.attr(type='gate')
+    def test_update_volume_metadata_item(self):
+        # Update metadata item for the volume
+        metadata = {"key1": "value1",
+                    "key2": "value2",
+                    "key3": "value3"}
+        create_expect = {"key1": "value1",
+                         "key2": "value2",
+                         "key3": "value3"}
+        update_item = {"key3": "value3_update"}
+        expect = {"key1": "value1",
+                  "key2": "value2",
+                  "key3": "value3_update"}
+        # Create metadata for the volume
+        resp, body = self.volumes_client.create_volume_metadata(
+            self.volume_id,
+            metadata)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(create_expect, body)
+        # Update metadata item
+        resp, body = self.volumes_client.update_volume_metadata_item(
+            self.volume_id,
+            "key3",
+            update_item)
+        self.assertEqual(200, resp.status)
+        # Get the metadata of the volume
+        resp, body = self.volumes_client.get_volume_metadata(self.volume_id)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(expect, body)
+
+
+class VolumeMetadataTestXML(VolumeMetadataTest):
+    _interface = "xml"
diff --git a/tempest/cli/__init__.py b/tempest/cli/__init__.py
index ec8b3a1..547d0d0 100644
--- a/tempest/cli/__init__.py
+++ b/tempest/cli/__init__.py
@@ -86,6 +86,12 @@
         return self.cmd_with_auth(
             'ceilometer', action, flags, params, admin, fail_ok)
 
+    def heat(self, action, flags='', params='', admin=True,
+             fail_ok=False):
+        """Executes heat command for the given action."""
+        return self.cmd_with_auth(
+            'heat', action, flags, params, admin, fail_ok)
+
     def cinder(self, action, flags='', params='', admin=True, fail_ok=False):
         """Executes cinder command for the given action."""
         return self.cmd_with_auth(
diff --git a/tempest/cli/simple_read_only/heat_templates/heat_minimal.yaml b/tempest/cli/simple_read_only/heat_templates/heat_minimal.yaml
new file mode 100644
index 0000000..7dcda39
--- /dev/null
+++ b/tempest/cli/simple_read_only/heat_templates/heat_minimal.yaml
@@ -0,0 +1,18 @@
+HeatTemplateFormatVersion: '2012-12-12'
+Description: Minimal template to test validation
+Parameters:
+  InstanceImage:
+    Description: Glance image name
+    Type: String
+  InstanceType:
+    Description: Nova instance type
+    Type: String
+    Default: m1.small
+    AllowedValues: [m1.tiny, m1.small, m1.medium, m1.large, m1.nano, m1.xlarge, m1.micro]
+    ConstraintDescription: must be a valid nova instance type.
+Resources:
+    InstanceResource:
+        Type: OS::Nova::Server
+        Properties:
+          flavor: {Ref: InstanceType}
+          image: {Ref: InstanceImage}
diff --git a/tempest/cli/simple_read_only/heat_templates/heat_minimal_hot.yaml b/tempest/cli/simple_read_only/heat_templates/heat_minimal_hot.yaml
new file mode 100644
index 0000000..6d89b7b
--- /dev/null
+++ b/tempest/cli/simple_read_only/heat_templates/heat_minimal_hot.yaml
@@ -0,0 +1,19 @@
+heat_template_version: 2013-05-23
+description: A minimal HOT test template
+parameters:
+  instance_image:
+    description: Glance image name
+    type: String
+  instance_type:
+    description: Nova instance type
+    type: String
+    default: m1.small
+    constraints:
+        - allowed_values: [m1.small, m1.medium, m1.large]
+          description: instance_type must be one of m1.small, m1.medium or m1.large
+resources:
+    instance:
+        type: OS::Nova::Server
+        properties:
+            image: { get_param: instance_image }
+            flavor: { get_param: instance_type }
diff --git a/tempest/cli/simple_read_only/test_heat.py b/tempest/cli/simple_read_only/test_heat.py
new file mode 100644
index 0000000..e2fefe8
--- /dev/null
+++ b/tempest/cli/simple_read_only/test_heat.py
@@ -0,0 +1,98 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 yaml
+
+from oslo.config import cfg
+
+import tempest.cli
+from tempest.openstack.common import log as logging
+
+CONF = cfg.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+class SimpleReadOnlyHeatClientTest(tempest.cli.ClientTestBase):
+    """Basic, read-only tests for Heat CLI client.
+
+    Basic smoke test for the heat CLI commands which do not require
+    creating or modifying stacks.
+    """
+
+    @classmethod
+    def setUpClass(cls):
+        if (not CONF.service_available.heat):
+            msg = ("Skipping all Heat cli tests because it is "
+                   "not available")
+            raise cls.skipException(msg)
+        super(SimpleReadOnlyHeatClientTest, cls).setUpClass()
+
+    def test_heat_stack_list(self):
+        self.heat('stack-list')
+
+    def test_heat_stack_list_debug(self):
+        self.heat('stack-list', flags='--debug')
+
+    def test_heat_resource_template_fmt_default(self):
+        ret = self.heat('resource-template OS::Nova::Server')
+        self.assertIn('Type: OS::Nova::Server', ret)
+
+    def test_heat_resource_template_fmt_arg_short_yaml(self):
+        ret = self.heat('resource-template -F yaml OS::Nova::Server')
+        self.assertIn('Type: OS::Nova::Server', ret)
+        self.assertIsInstance(yaml.safe_load(ret), dict)
+
+    def test_heat_resource_template_fmt_arg_long_json(self):
+        ret = self.heat('resource-template --format json OS::Nova::Server')
+        self.assertIn('"Type": "OS::Nova::Server",', ret)
+        self.assertIsInstance(json.loads(ret), dict)
+
+    def test_heat_resource_type_list(self):
+        ret = self.heat('resource-type-list')
+        rsrc_types = self.parser.listing(ret)
+        self.assertTableStruct(rsrc_types, ['resource_type'])
+
+    def test_heat_resource_type_show(self):
+        rsrc_schema = self.heat('resource-type-show OS::Nova::Server')
+        # resource-type-show returns a json resource schema
+        self.assertIsInstance(json.loads(rsrc_schema), dict)
+
+    def test_heat_template_validate_yaml(self):
+        filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                'heat_templates/heat_minimal.yaml')
+        ret = self.heat('template-validate -f %s' % filepath)
+        # On success template-validate returns a json representation
+        # of the template parameters
+        self.assertIsInstance(json.loads(ret), dict)
+
+    def test_heat_template_validate_hot(self):
+        filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)),
+                                'heat_templates/heat_minimal_hot.yaml')
+        ret = self.heat('template-validate -f %s' % filepath)
+        self.assertIsInstance(json.loads(ret), dict)
+
+    def test_heat_help(self):
+        self.heat('help')
+
+    def test_heat_help_cmd(self):
+        # Check requesting help for a specific command works
+        help_text = self.heat('help resource-template')
+        lines = help_text.split('\n')
+        self.assertFirstLineStartsWith(lines, 'usage: heat resource-template')
+
+    def test_heat_version(self):
+        self.heat('', flags='--version')
diff --git a/tempest/cli/simple_read_only/test_neutron.py b/tempest/cli/simple_read_only/test_neutron.py
index 80376ab..61ffc25 100644
--- a/tempest/cli/simple_read_only/test_neutron.py
+++ b/tempest/cli/simple_read_only/test_neutron.py
@@ -76,11 +76,13 @@
 
     @test.skip_because(bug="1240694")
     @test.attr(type='smoke')
+    @test.requires_ext(extension='metering', service='network')
     def test_neutron_meter_label_list(self):
         self.neutron('meter-label-list')
 
     @test.skip_because(bug="1240694")
     @test.attr(type='smoke')
+    @test.requires_ext(extension='metering', service='network')
     def test_neutron_meter_label_rule_list(self):
         self.neutron('meter-label-rule-list')
 
diff --git a/tempest/clients.py b/tempest/clients.py
index 83b72c6..519d191 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -18,6 +18,8 @@
 from tempest import config
 from tempest import exceptions
 from tempest.openstack.common import log as logging
+from tempest.services.baremetal.v1.client_json import BaremetalClientJSON
+from tempest.services.baremetal.v1.client_xml import BaremetalClientXML
 from tempest.services import botoclients
 from tempest.services.compute.json.aggregates_client import \
     AggregatesClientJSON
@@ -232,6 +234,7 @@
         if interface == 'xml':
             self.certificates_client = CertificatesClientXML(*client_args)
             self.certificates_v3_client = CertificatesV3ClientXML(*client_args)
+            self.baremetal_client = BaremetalClientXML(*client_args)
             self.servers_client = ServersClientXML(*client_args)
             self.servers_v3_client = ServersV3ClientXML(*client_args)
             self.limits_client = LimitsClientXML(*client_args)
@@ -294,6 +297,7 @@
             self.certificates_client = CertificatesClientJSON(*client_args)
             self.certificates_v3_client = CertificatesV3ClientJSON(
                 *client_args)
+            self.baremetal_client = BaremetalClientJSON(*client_args)
             self.servers_client = ServersClientJSON(*client_args)
             self.servers_v3_client = ServersV3ClientJSON(*client_args)
             self.limits_client = LimitsClientJSON(*client_args)
diff --git a/tempest/common/custom_matchers.py b/tempest/common/custom_matchers.py
index 307d5db..81b5153 100644
--- a/tempest/common/custom_matchers.py
+++ b/tempest/common/custom_matchers.py
@@ -125,7 +125,7 @@
             elif key == 'content-type' and not value:
                 return InvalidFormat(key, value)
             elif key == 'x-trans-id' and \
-                not re.match("^tx[0-9a-f]*-[0-9a-f]*$", value):
+                not re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*", value):
                 return InvalidFormat(key, value)
             elif key == 'date' and not value:
                 return InvalidFormat(key, value)
diff --git a/tempest/common/utils/data_utils.py b/tempest/common/utils/data_utils.py
index 4f93e1c..339d22a 100644
--- a/tempest/common/utils/data_utils.py
+++ b/tempest/common/utils/data_utils.py
@@ -40,6 +40,21 @@
     return random.randint(start, end)
 
 
+def rand_mac_address():
+    """Generate an Ethernet MAC address."""
+    # NOTE(vish): We would prefer to use 0xfe here to ensure that linux
+    #             bridge mac addresses don't change, but it appears to
+    #             conflict with libvirt, so we use the next highest octet
+    #             that has the unicast and locally administered bits set
+    #             properly: 0xfa.
+    #             Discussion: https://bugs.launchpad.net/nova/+bug/921838
+    mac = [0xfa, 0x16, 0x3e,
+           random.randint(0x00, 0xff),
+           random.randint(0x00, 0xff),
+           random.randint(0x00, 0xff)]
+    return ':'.join(["%02x" % x for x in mac])
+
+
 def build_url(host, port, api_version=None, path=None,
               params=None, use_ssl=False):
     """Build the request URL from given host, port, path and parameters."""
diff --git a/tempest/config.py b/tempest/config.py
index d42edc9..8d6e52a 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -644,6 +644,9 @@
     cfg.BoolOpt('savanna',
                 default=False,
                 help="Whether or not Savanna is expected to be available"),
+    cfg.BoolOpt('ironic',
+                default=False,
+                help="Whether or not Ironic is expected to be available"),
 ]
 
 debug_group = cfg.OptGroup(name="debug",
@@ -656,6 +659,16 @@
 ]
 
 
+baremetal_group = cfg.OptGroup(name='baremetal',
+                               title='Baremetal provisioning service options')
+
+BaremetalGroup = [
+    cfg.StrOpt('catalog_type',
+               default='baremetal',
+               help="Catalog type of the baremetal provisioning service."),
+]
+
+
 # this should never be called outside of this class
 class TempestConfigPrivate(object):
     """Provides OpenStack configuration information."""
@@ -721,6 +734,8 @@
         register_opt_group(cfg.CONF, service_available_group,
                            ServiceAvailableGroup)
         register_opt_group(cfg.CONF, debug_group, DebugGroup)
+        register_opt_group(cfg.CONF, baremetal_group, BaremetalGroup)
+
         self.compute = cfg.CONF.compute
         self.compute_feature_enabled = cfg.CONF['compute-feature-enabled']
         self.identity = cfg.CONF.identity
@@ -743,6 +758,8 @@
         self.scenario = cfg.CONF.scenario
         self.service_available = cfg.CONF.service_available
         self.debug = cfg.CONF.debug
+        self.baremetal = cfg.CONF.baremetal
+
         if not self.compute_admin.username:
             self.compute_admin.username = self.identity.admin_username
             self.compute_admin.password = self.identity.admin_password
diff --git a/tempest/services/baremetal/__init__.py b/tempest/services/baremetal/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/baremetal/__init__.py
diff --git a/tempest/services/baremetal/base.py b/tempest/services/baremetal/base.py
new file mode 100644
index 0000000..3d4fa50
--- /dev/null
+++ b/tempest/services/baremetal/base.py
@@ -0,0 +1,197 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 functools
+import json
+
+import six
+
+from tempest.common import rest_client
+
+
+def handle_errors(f):
+    """A decorator that allows to ignore certain types of errors."""
+
+    @functools.wraps(f)
+    def wrapper(*args, **kwargs):
+        param_name = 'ignore_errors'
+        ignored_errors = kwargs.get(param_name, tuple())
+
+        if param_name in kwargs:
+            del kwargs[param_name]
+
+        try:
+            return f(*args, **kwargs)
+        except ignored_errors:
+            # Silently ignore errors
+            pass
+
+    return wrapper
+
+
+class BaremetalClient(rest_client.RestClient):
+    """
+    Base Tempest REST client for Ironic API.
+
+    """
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(BaremetalClient, self).__init__(config, username, password,
+                                              auth_url, tenant_name)
+        self.service = self.config.baremetal.catalog_type
+        self.uri_prefix = ''
+
+    def serialize(self, object_type, object_dict):
+        """Serialize an Ironic object."""
+
+        raise NotImplementedError
+
+    def deserialize(self, object_str):
+        """Deserialize an Ironic object."""
+
+        raise NotImplementedError
+
+    def _get_uri(self, resource_name, uuid=None, permanent=False):
+        """
+        Get URI for a specific resource or object.
+
+        :param resource_name: The name of the REST resource, e.g., 'nodes'.
+        :param uuid: The unique identifier of an object in UUID format.
+        :return: Relative URI for the resource or object.
+
+        """
+        prefix = self.uri_prefix if not permanent else ''
+
+        return '{pref}/{res}{uuid}'.format(pref=prefix,
+                                           res=resource_name,
+                                           uuid='/%s' % uuid if uuid else '')
+
+    def _make_patch(self, allowed_attributes, **kw):
+        """
+        Create a JSON patch according to RFC 6902.
+
+        :param allowed_attributes: An iterable object that contains a set of
+            allowed attributes for an object.
+        :param **kw: Attributes and new values for them.
+        :return: A JSON path that sets values of the specified attributes to
+            the new ones.
+
+        """
+        def get_change(kw, path='/'):
+            for name, value in six.iteritems(kw):
+                if isinstance(value, dict):
+                    for ch in get_change(value, path + '%s/' % name):
+                        yield ch
+                else:
+                    yield {'path': path + name,
+                           'value': value,
+                           'op': 'replace'}
+
+        patch = [ch for ch in get_change(kw)
+                 if ch['path'].lstrip('/') in allowed_attributes]
+
+        return patch
+
+    def _list_request(self, resource, permanent=False):
+        """
+        Get the list of objects of the specified type.
+
+        :param resource: The name of the REST resource, e.g., 'nodes'.
+        :return: A tuple with the server response and deserialized JSON list
+                 of objects
+
+        """
+        uri = self._get_uri(resource, permanent=permanent)
+
+        resp, body = self.get(uri, self.headers)
+
+        return resp, self.deserialize(body)
+
+    def _show_request(self, resource, uuid, permanent=False):
+        """
+        Gets a specific object of the specified type.
+
+        :param uuid: Unique identifier of the object in UUID format.
+        :return: Serialized object as a dictionary.
+
+        """
+        uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
+        resp, body = self.get(uri, self.headers)
+
+        return resp, self.deserialize(body)
+
+    def _create_request(self, resource, object_type, object_dict):
+        """
+        Create an object of the specified type.
+
+        :param resource: The name of the REST resource, e.g., 'nodes'.
+        :param object_dict: A Python dict that represents an object of the
+                            specified type.
+        :return: A tuple with the server response and the deserialized created
+                 object.
+
+        """
+        body = self.serialize(object_type, object_dict)
+        uri = self._get_uri(resource)
+
+        resp, body = self.post(uri, headers=self.headers, body=body)
+
+        return resp, self.deserialize(body)
+
+    def _delete_request(self, resource, uuid):
+        """
+        Delete specified object.
+
+        :param resource: The name of the REST resource, e.g., 'nodes'.
+        :param uuid: The unique identifier of an object in UUID format.
+        :return: A tuple with the server response and the response body.
+
+        """
+        uri = self._get_uri(resource, uuid)
+
+        resp, body = self.delete(uri, self.headers)
+        return resp, body
+
+    def _patch_request(self, resource, uuid, patch_object):
+        """
+        Update specified object with JSON-patch.
+
+        :param resource: The name of the REST resource, e.g., 'nodes'.
+        :param uuid: The unique identifier of an object in UUID format.
+        :return: A tuple with the server response and the serialized patched
+                 object.
+
+        """
+        uri = self._get_uri(resource, uuid)
+        patch_body = json.dumps(patch_object)
+
+        resp, body = self.patch(uri, headers=self.headers, body=patch_body)
+        return resp, self.deserialize(body)
+
+    @handle_errors
+    def get_api_description(self):
+        """Retrieves all versions of the Ironic API."""
+
+        return self._list_request('', permanent=True)
+
+    @handle_errors
+    def get_version_description(self, version='v1'):
+        """
+        Retrieves the desctription of the API.
+
+        :param version: The version of the API. Default: 'v1'.
+        :return: Serialized description of API resources.
+
+        """
+        return self._list_request(version, permanent=True)
diff --git a/tempest/services/baremetal/v1/__init__.py b/tempest/services/baremetal/v1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/baremetal/v1/__init__.py
diff --git a/tempest/services/baremetal/v1/base_v1.py b/tempest/services/baremetal/v1/base_v1.py
new file mode 100644
index 0000000..5fdf036
--- /dev/null
+++ b/tempest/services/baremetal/v1/base_v1.py
@@ -0,0 +1,209 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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.services.baremetal import base
+
+
+class BaremetalClientV1(base.BaremetalClient):
+    """
+    Base Tempest REST client for Ironic API v1.
+
+    Specific implementations must implement serialize and deserialize
+    methods in order to send requests to Ironic.
+
+    """
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(BaremetalClientV1, self).__init__(config, username, password,
+                                                auth_url, tenant_name)
+        self.version = '1'
+        self.uri_prefix = 'v%s' % self.version
+
+    @base.handle_errors
+    def list_nodes(self):
+        """List all existing nodes."""
+        return self._list_request('nodes')
+
+    @base.handle_errors
+    def list_chassis(self):
+        """List all existing chassis."""
+        return self._list_request('chassis')
+
+    @base.handle_errors
+    def list_ports(self):
+        """List all existing ports."""
+        return self._list_request('ports')
+
+    @base.handle_errors
+    def show_node(self, uuid):
+        """
+        Gets a specific node.
+
+        :param uuid: Unique identifier of the node in UUID format.
+        :return: Serialized node as a dictionary.
+
+        """
+        return self._show_request('nodes', uuid)
+
+    @base.handle_errors
+    def show_chassis(self, uuid):
+        """
+        Gets a specific chassis.
+
+        :param uuid: Unique identifier of the chassis in UUID format.
+        :return: Serialized chassis as a dictionary.
+
+        """
+        return self._show_request('chassis', uuid)
+
+    @base.handle_errors
+    def show_port(self, uuid):
+        """
+        Gets a specific port.
+
+        :param uuid: Unique identifier of the port in UUID format.
+        :return: Serialized port as a dictionary.
+
+        """
+        return self._show_request('ports', uuid)
+
+    @base.handle_errors
+    def create_node(self, chassis_id, **kwargs):
+        """
+        Create a baremetal node with the specified parameters.
+
+        :param cpu_arch: CPU architecture of the node. Default: x86_64.
+        :param cpu_num: Number of CPUs. Default: 8.
+        :param storage: Disk size. Default: 1024.
+        :param memory: Available RAM. Default: 4096.
+        :param driver: Driver name. Default: "fake"
+        :return: A tuple with the server response and the created node.
+
+        """
+        node = {'chassis_uuid': chassis_id,
+                'properties': {'cpu_arch': kwargs.get('cpu_arch', 'x86_64'),
+                               'cpu_num': kwargs.get('cpu_num', 8),
+                               'storage': kwargs.get('storage', 1024),
+                               'memory': kwargs.get('memory', 4096)},
+                'driver': kwargs.get('driver', 'fake')}
+
+        return self._create_request('nodes', 'node', node)
+
+    @base.handle_errors
+    def create_chassis(self, **kwargs):
+        """
+        Create a chassis with the specified parameters.
+
+        :param description: The description of the chassis.
+            Default: test-chassis
+        :return: A tuple with the server response and the created chassis.
+
+        """
+        chassis = {'description': kwargs.get('description', 'test-chassis')}
+
+        return self._create_request('chassis', 'chassis', chassis)
+
+    @base.handle_errors
+    def create_port(self, node_id, **kwargs):
+        """
+        Create a port with the specified parameters.
+
+        :param node_id: The ID of the node which owns the port.
+        :param address: MAC address of the port. Default: 01:23:45:67:89:0A.
+        :return: A tuple with the server response and the created port.
+
+        """
+        port = {'address': kwargs.get('address', '01:23:45:67:89:0A'),
+                'node_uuid': node_id}
+
+        return self._create_request('ports', 'port', port)
+
+    @base.handle_errors
+    def delete_node(self, uuid):
+        """
+        Deletes a node having the specified UUID.
+
+        :param uuid: The unique identifier of the node.
+        :return: A tuple with the server response and the response body.
+
+        """
+        return self._delete_request('nodes', uuid)
+
+    @base.handle_errors
+    def delete_chassis(self, uuid):
+        """
+        Deletes a chassis having the specified UUID.
+
+        :param uuid: The unique identifier of the chassis.
+        :return: A tuple with the server response and the response body.
+
+        """
+        return self._delete_request('chassis', uuid)
+
+    @base.handle_errors
+    def delete_port(self, uuid):
+        """
+        Deletes a port having the specified UUID.
+
+        :param uuid: The unique identifier of the port.
+        :return: A tuple with the server response and the response body.
+
+        """
+        return self._delete_request('ports', uuid)
+
+    @base.handle_errors
+    def update_node(self, uuid, **kwargs):
+        """
+        Update the specified node.
+
+        :param uuid: The unique identifier of the node.
+        :return: A tuple with the server response and the updated node.
+
+        """
+        node_attributes = ('properties/cpu_arch',
+                           'properties/cpu_num',
+                           'properties/storage',
+                           'properties/memory',
+                           'driver')
+
+        patch = self._make_patch(node_attributes, **kwargs)
+
+        return self._patch_request('nodes', uuid, patch)
+
+    @base.handle_errors
+    def update_chassis(self, uuid, **kwargs):
+        """
+        Update the specified chassis.
+
+        :param uuid: The unique identifier of the chassis.
+        :return: A tuple with the server response and the updated chassis.
+
+        """
+        chassis_attributes = ('description',)
+        patch = self._make_patch(chassis_attributes, **kwargs)
+
+        return self._patch_request('chassis', uuid, patch)
+
+    @base.handle_errors
+    def update_port(self, uuid, **kwargs):
+        """
+        Update the specified port.
+
+        :param uuid: The unique identifier of the port.
+        :return: A tuple with the server response and the updated port.
+
+        """
+        port_attributes = ('address',)
+        patch = self._make_patch(port_attributes, **kwargs)
+
+        return self._patch_request('ports', uuid, patch)
diff --git a/tempest/services/baremetal/v1/client_json.py b/tempest/services/baremetal/v1/client_json.py
new file mode 100644
index 0000000..fa7cd67
--- /dev/null
+++ b/tempest/services/baremetal/v1/client_json.py
@@ -0,0 +1,28 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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
+
+from tempest.services.baremetal.v1 import base_v1
+
+
+class BaremetalClientJSON(base_v1.BaremetalClientV1):
+    """Tempest REST client for Ironic JSON API v1."""
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(BaremetalClientJSON, self).__init__(config, username, password,
+                                                  auth_url, tenant_name)
+
+        self.serialize = lambda obj_type, obj_body: json.dumps(obj_body)
+        self.deserialize = json.loads
diff --git a/tempest/services/baremetal/v1/client_xml.py b/tempest/services/baremetal/v1/client_xml.py
new file mode 100644
index 0000000..a9b5a77
--- /dev/null
+++ b/tempest/services/baremetal/v1/client_xml.py
@@ -0,0 +1,57 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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.common import rest_client
+from tempest.services.baremetal.v1 import base_v1 as base
+from tempest.services.compute.xml import common as xml
+
+
+class BaremetalClientXML(rest_client.RestClientXML, base.BaremetalClientV1):
+    """Tempest REST client for Ironic XML API v1."""
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(BaremetalClientXML, self).__init__(config, username, password,
+                                                 auth_url, tenant_name)
+
+        self.serialize = self.json_to_xml
+        self.deserialize = xml.xml_to_json
+
+    def json_to_xml(self, object_type, object_dict):
+        """
+        Brainlessly converts a specification of an object to XML string.
+
+        :param object_type: Kind of the object.
+        :param object_dict: Specification of the object attributes as a dict.
+        :return: An XML string that corresponds to the specification.
+
+        """
+        root = xml.Element(object_type)
+
+        for attr_name, value in object_dict:
+            # Handle nested dictionaries
+            if isinstance(value, dict):
+                value = self.json_to_xml(attr_name, value)
+
+            root.append(xml.Element(attr_name, value))
+
+        return str(xml.Document(root))
+
+    def _patch_request(self, resource_name, uuid, patch_object):
+        """Changes Content-Type header to application/json for jsonpatch."""
+
+        self.headers['Content-Type'] = 'application/json'
+        try:
+            super(self)._patch_request(self, resource_name, uuid, patch_object)
+        finally:
+            self.headers['Content-Type'] = 'application/xml'
diff --git a/tempest/services/compute/json/security_groups_client.py b/tempest/services/compute/json/security_groups_client.py
index 1f01437..361ec36 100644
--- a/tempest/services/compute/json/security_groups_client.py
+++ b/tempest/services/compute/json/security_groups_client.py
@@ -63,6 +63,25 @@
         body = json.loads(body)
         return resp, body['security_group']
 
+    def update_security_group(self, security_group_id, name=None,
+                              description=None):
+        """
+        Update a security group.
+        security_group_id: a security_group to update
+        name: new name of security group
+        description: new description of security group
+        """
+        post_body = {}
+        if name:
+            post_body['name'] = name
+        if description:
+            post_body['description'] = description
+        post_body = json.dumps({'security_group': post_body})
+        resp, body = self.put('os-security-groups/%s' % str(security_group_id),
+                              post_body, self.headers)
+        body = json.loads(body)
+        return resp, body['security_group']
+
     def delete_security_group(self, security_group_id):
         """Deletes the provided Security Group."""
         return self.delete('os-security-groups/%s' % str(security_group_id))
diff --git a/tempest/services/compute/xml/security_groups_client.py b/tempest/services/compute/xml/security_groups_client.py
index 5d86790..aebeb4d 100644
--- a/tempest/services/compute/xml/security_groups_client.py
+++ b/tempest/services/compute/xml/security_groups_client.py
@@ -79,6 +79,30 @@
         body = self._parse_body(etree.fromstring(body))
         return resp, body
 
+    def update_security_group(self, security_group_id, name=None,
+                              description=None):
+        """
+        Update a security group.
+        security_group_id: a security_group to update
+        name: new name of security group
+        description: new description of security group
+        """
+        security_group = Element("security_group")
+        if name:
+            sg_name = Element("name")
+            sg_name.append(Text(content=name))
+            security_group.append(sg_name)
+        if description:
+            des = Element("description")
+            des.append(Text(content=description))
+            security_group.append(des)
+        resp, body = self.put('os-security-groups/%s' %
+                              str(security_group_id),
+                              str(Document(security_group)),
+                              self.headers)
+        body = self._parse_body(etree.fromstring(body))
+        return resp, body
+
     def delete_security_group(self, security_group_id):
         """Deletes the provided Security Group."""
         return self.delete('os-security-groups/%s' %
diff --git a/tempest/services/volume/json/snapshots_client.py b/tempest/services/volume/json/snapshots_client.py
index 9435122..c270de8 100644
--- a/tempest/services/volume/json/snapshots_client.py
+++ b/tempest/services/volume/json/snapshots_client.py
@@ -149,3 +149,40 @@
         url = 'snapshots/%s/action' % str(snapshot_id)
         resp, body = self.post(url, post_body, self.headers)
         return resp, body
+
+    def create_snapshot_metadata(self, snapshot_id, metadata):
+        """Create metadata for the snapshot."""
+        put_body = json.dumps({'metadata': metadata})
+        url = "snapshots/%s/metadata" % str(snapshot_id)
+        resp, body = self.post(url, put_body, self.headers)
+        body = json.loads(body)
+        return resp, body['metadata']
+
+    def get_snapshot_metadata(self, snapshot_id):
+        """Get metadata of the snapshot."""
+        url = "snapshots/%s/metadata" % str(snapshot_id)
+        resp, body = self.get(url, self.headers)
+        body = json.loads(body)
+        return resp, body['metadata']
+
+    def update_snapshot_metadata(self, snapshot_id, metadata):
+        """Update metadata for the snapshot."""
+        put_body = json.dumps({'metadata': metadata})
+        url = "snapshots/%s/metadata" % str(snapshot_id)
+        resp, body = self.put(url, put_body, self.headers)
+        body = json.loads(body)
+        return resp, body['metadata']
+
+    def update_snapshot_metadata_item(self, snapshot_id, id, meta_item):
+        """Update metadata item for the snapshot."""
+        put_body = json.dumps({'meta': meta_item})
+        url = "snapshots/%s/metadata/%s" % (str(snapshot_id), str(id))
+        resp, body = self.put(url, put_body, self.headers)
+        body = json.loads(body)
+        return resp, body['meta']
+
+    def delete_snapshot_metadata_item(self, snapshot_id, id):
+        """Delete metadata item for the snapshot."""
+        url = "snapshots/%s/metadata/%s" % (str(snapshot_id), str(id))
+        resp, body = self.delete(url, self.headers)
+        return resp, body
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index 967dc09..afba4b0 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -263,3 +263,40 @@
         resp, body = self.post('volumes/%s/action' % volume_id, post_body,
                                self.headers)
         return resp, body
+
+    def create_volume_metadata(self, volume_id, metadata):
+        """Create metadata for the volume."""
+        put_body = json.dumps({'metadata': metadata})
+        url = "volumes/%s/metadata" % str(volume_id)
+        resp, body = self.post(url, put_body, self.headers)
+        body = json.loads(body)
+        return resp, body['metadata']
+
+    def get_volume_metadata(self, volume_id):
+        """Get metadata of the volume."""
+        url = "volumes/%s/metadata" % str(volume_id)
+        resp, body = self.get(url, self.headers)
+        body = json.loads(body)
+        return resp, body['metadata']
+
+    def update_volume_metadata(self, volume_id, metadata):
+        """Update metadata for the volume."""
+        put_body = json.dumps({'metadata': metadata})
+        url = "volumes/%s/metadata" % str(volume_id)
+        resp, body = self.put(url, put_body, self.headers)
+        body = json.loads(body)
+        return resp, body['metadata']
+
+    def update_volume_metadata_item(self, volume_id, id, meta_item):
+        """Update metadata item for the volume."""
+        put_body = json.dumps({'meta': meta_item})
+        url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
+        resp, body = self.put(url, put_body, self.headers)
+        body = json.loads(body)
+        return resp, body['meta']
+
+    def delete_volume_metadata_item(self, volume_id, id):
+        """Delete metadata item for the volume."""
+        url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
+        resp, body = self.delete(url, self.headers)
+        return resp, body
diff --git a/tempest/services/volume/xml/snapshots_client.py b/tempest/services/volume/xml/snapshots_client.py
index 5d59b07..3a70eab 100644
--- a/tempest/services/volume/xml/snapshots_client.py
+++ b/tempest/services/volume/xml/snapshots_client.py
@@ -22,6 +22,7 @@
 from tempest.openstack.common import log as logging
 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
 from tempest.services.compute.xml.common import XMLNS_11
 
@@ -170,3 +171,57 @@
         if body:
             body = xml_to_json(etree.fromstring(body))
         return resp, body
+
+    def _metadata_body(self, meta):
+        post_body = Element('metadata')
+        for k, v in meta.items():
+            data = Element('meta', key=k)
+            data.append(Text(v))
+            post_body.append(data)
+        return post_body
+
+    def _parse_key_value(self, node):
+        """Parse <foo key='key'>value</foo> data into {'key': 'value'}."""
+        data = {}
+        for node in node.getchildren():
+            data[node.get('key')] = node.text
+        return data
+
+    def create_snapshot_metadata(self, snapshot_id, metadata):
+        """Create metadata for the snapshot."""
+        post_body = self._metadata_body(metadata)
+        resp, body = self.post('snapshots/%s/metadata' % snapshot_id,
+                               str(Document(post_body)),
+                               self.headers)
+        body = self._parse_key_value(etree.fromstring(body))
+        return resp, body
+
+    def get_snapshot_metadata(self, snapshot_id):
+        """Get metadata of the snapshot."""
+        url = "snapshots/%s/metadata" % str(snapshot_id)
+        resp, body = self.get(url, self.headers)
+        body = self._parse_key_value(etree.fromstring(body))
+        return resp, body
+
+    def update_snapshot_metadata(self, snapshot_id, metadata):
+        """Update metadata for the snapshot."""
+        put_body = self._metadata_body(metadata)
+        url = "snapshots/%s/metadata" % str(snapshot_id)
+        resp, body = self.put(url, str(Document(put_body)), self.headers)
+        body = self._parse_key_value(etree.fromstring(body))
+        return resp, body
+
+    def update_snapshot_metadata_item(self, snapshot_id, id, meta_item):
+        """Update metadata item for the snapshot."""
+        for k, v in meta_item.items():
+            put_body = Element('meta', key=k)
+            put_body.append(Text(v))
+        url = "snapshots/%s/metadata/%s" % (str(snapshot_id), str(id))
+        resp, body = self.put(url, str(Document(put_body)), self.headers)
+        body = xml_to_json(etree.fromstring(body))
+        return resp, body
+
+    def delete_snapshot_metadata_item(self, snapshot_id, id):
+        """Delete metadata item for the snapshot."""
+        url = "snapshots/%s/metadata/%s" % (str(snapshot_id), str(id))
+        return self.delete(url)
diff --git a/tempest/services/volume/xml/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
index 1fc63e9..f175138 100644
--- a/tempest/services/volume/xml/volumes_client.py
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -356,3 +356,57 @@
         if body:
             body = xml_to_json(etree.fromstring(body))
         return resp, body
+
+    def _metadata_body(self, meta):
+        post_body = Element('metadata')
+        for k, v in meta.items():
+            data = Element('meta', key=k)
+            data.append(Text(v))
+            post_body.append(data)
+        return post_body
+
+    def _parse_key_value(self, node):
+        """Parse <foo key='key'>value</foo> data into {'key': 'value'}."""
+        data = {}
+        for node in node.getchildren():
+            data[node.get('key')] = node.text
+        return data
+
+    def create_volume_metadata(self, volume_id, metadata):
+        """Create metadata for the volume."""
+        post_body = self._metadata_body(metadata)
+        resp, body = self.post('volumes/%s/metadata' % volume_id,
+                               str(Document(post_body)),
+                               self.headers)
+        body = self._parse_key_value(etree.fromstring(body))
+        return resp, body
+
+    def get_volume_metadata(self, volume_id):
+        """Get metadata of the volume."""
+        url = "volumes/%s/metadata" % str(volume_id)
+        resp, body = self.get(url, self.headers)
+        body = self._parse_key_value(etree.fromstring(body))
+        return resp, body
+
+    def update_volume_metadata(self, volume_id, metadata):
+        """Update metadata for the volume."""
+        put_body = self._metadata_body(metadata)
+        url = "volumes/%s/metadata" % str(volume_id)
+        resp, body = self.put(url, str(Document(put_body)), self.headers)
+        body = self._parse_key_value(etree.fromstring(body))
+        return resp, body
+
+    def update_volume_metadata_item(self, volume_id, id, meta_item):
+        """Update metadata item for the volume."""
+        for k, v in meta_item.items():
+            put_body = Element('meta', key=k)
+            put_body.append(Text(v))
+        url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
+        resp, body = self.put(url, str(Document(put_body)), self.headers)
+        body = xml_to_json(etree.fromstring(body))
+        return resp, body
+
+    def delete_volume_metadata_item(self, volume_id, id):
+        """Delete metadata item for the volume."""
+        url = "volumes/%s/metadata/%s" % (str(volume_id), str(id))
+        return self.delete(url)
diff --git a/tempest/stress/etc/ssh_floating.json b/tempest/stress/etc/ssh_floating.json
index 0cb6776..e03fd4f 100644
--- a/tempest/stress/etc/ssh_floating.json
+++ b/tempest/stress/etc/ssh_floating.json
@@ -8,7 +8,7 @@
              "new_floating": true,
              "verify": ["check_icmp_echo", "check_port_ssh"],
              "check_timeout": 120,
-             "check_inerval": 1,
+             "check_interval": 1,
              "wait_after_vm_create": true,
              "wait_for_disassociate": true,
              "reboot": false}
diff --git a/tox.ini b/tox.ini
index b44b3e0..6d596e3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,13 +4,14 @@
 skipsdist = True
 
 [testenv]
+sitepackages = True
 setenv = VIRTUAL_ENV={envdir}
          LANG=en_US.UTF-8
          LANGUAGE=en_US:en
          LC_ALL=C
          OS_TEST_PATH=./tempest/test_discover
 usedevelop = True
-install_command = pip install -U {opts} {packages}
+install_command = pip install {opts} {packages}
 
 [testenv:py26]
 setenv = OS_TEST_PATH=./tempest/tests
@@ -25,38 +26,32 @@
 commands = python setup.py test --slowest --testr-arg='tempest\.tests {posargs}'
 
 [testenv:all]
-sitepackages = True
 setenv = VIRTUAL_ENV={envdir}
 commands =
   python setup.py testr --slowest --testr-args='{posargs}'
 
 [testenv:full]
-sitepackages = True
 # The regex below is used to select which tests to run and exclude the slow tag:
 # See the testrepostiory bug: https://bugs.launchpad.net/testrepository/+bug/1208610
 commands =
   bash tools/pretty_tox.sh '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario|thirdparty|cli)) {posargs}'
 
 [testenv:testr-full]
-sitepackages = True
 commands =
   bash tools/pretty_tox.sh '(?!.*\[.*\bslow\b.*\])(^tempest\.(api|scenario|thirdparty|cli)) {posargs}'
 
 [testenv:heat-slow]
-sitepackages = True
 setenv = OS_TEST_TIMEOUT=1200
 # The regex below is used to select heat api/scenario tests tagged as slow.
 commands =
   bash tools/pretty_tox_serial.sh '(?=.*\[.*\bslow\b.*\])(^tempest\.(api|scenario)\.orchestration) {posargs}'
 
 [testenv:large-ops]
-sitepackages = True
 commands =
   python setup.py testr --slowest --testr-args='tempest.scenario.test_large_ops {posargs}'
 
 
 [testenv:py26-full]
-sitepackages = True
 setenv = VIRTUAL_ENV={envdir}
          NOSE_WITH_OPENSTACK=1
          NOSE_OPENSTACK_COLOR=1
@@ -81,12 +76,10 @@
   nosetests --logging-format '%(asctime)-15s %(message)s' --with-xunit -sv --attr=type=smoke --xunit-file=nosetests-smoke.xml tempest {posargs}
 
 [testenv:smoke]
-sitepackages = True
 commands =
    bash tools/pretty_tox.sh '(?!.*\[.*\bslow\b.*\])((smoke)|(^tempest\.scenario)) {posargs}'
 
 [testenv:smoke-serial]
-sitepackages = True
 # This is still serial because neutron doesn't work with parallel. See:
 # https://bugs.launchpad.net/tempest/+bug/1216076 so the neutron smoke
 # job would fail if we moved it to parallel.
@@ -94,7 +87,6 @@
    bash tools/pretty_tox_serial.sh '(?!.*\[.*\bslow\b.*\])((smoke)|(^tempest\.scenario)) {posargs}'
 
 [testenv:stress]
-sitepackages = True
 commands =
     python -m tempest/stress/run_stress -a -d 3600 -S