Merge "port some servers tests into nova v3 part2"
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index d04c432..f6618a1 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -271,6 +271,8 @@
         cls.volumes_client = cls.os.volumes_client
         cls.certificates_client = cls.os.certificates_v3_client
         cls.keypairs_client = cls.os.keypairs_v3_client
+        cls.aggregates_client = cls.os.aggregates_v3_client
+        cls.hosts_client = cls.os.hosts_v3_client
 
     @classmethod
     def create_image_from_server(cls, server_id, **kwargs):
@@ -329,10 +331,12 @@
             os_adm = clients.ComputeAdminManager(interface=cls._interface)
 
         cls.os_adm = os_adm
-        cls.severs_admin_client = cls.os_adm.servers_v3_client
+        cls.servers_admin_client = cls.os_adm.servers_v3_client
         cls.services_admin_client = cls.os_adm.services_v3_client
         cls.availability_zone_admin_client = \
             cls.os_adm.availability_zone_v3_client
         cls.hypervisor_admin_client = cls.os_adm.hypervisor_v3_client
         cls.tenant_usages_admin_client = cls.os_adm.tenant_usages_v3_client
         cls.flavors_admin_client = cls.os_adm.flavors_v3_client
+        cls.aggregates_admin_client = cls.os_adm.aggregates_v3_client
+        cls.hosts_admin_client = cls.os_adm.hosts_v3_client
diff --git a/tempest/api/compute/v3/admin/test_aggregates.py b/tempest/api/compute/v3/admin/test_aggregates.py
new file mode 100644
index 0000000..144dc44
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_aggregates.py
@@ -0,0 +1,221 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation.
+# 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 import tempest_fixtures as fixtures
+from tempest.common.utils import data_utils
+from tempest import test
+
+
+class AggregatesAdminV3TestJSON(base.BaseV3ComputeAdminTest):
+
+    """
+    Tests Aggregates API that require admin privileges
+    """
+
+    _host_key = 'os-extended-server-attributes:host'
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(AggregatesAdminV3TestJSON, cls).setUpClass()
+        cls.client = cls.aggregates_admin_client
+        cls.user_client = cls.aggregates_client
+        cls.aggregate_name_prefix = 'test_aggregate_'
+        cls.az_name_prefix = 'test_az_'
+
+        resp, hosts_all = cls.hosts_admin_client.list_hosts()
+        hosts = map(lambda x: x['host_name'],
+                    filter(lambda y: y['service'] == 'compute', hosts_all))
+        cls.host = hosts[0]
+
+    @test.attr(type='gate')
+    def test_aggregate_create_delete(self):
+        # Create and delete an aggregate.
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.assertEqual(201, resp.status)
+        self.assertEqual(aggregate_name, aggregate['name'])
+        self.assertEqual(None, aggregate['availability_zone'])
+
+        resp, _ = self.client.delete_aggregate(aggregate['id'])
+        self.assertEqual(204, resp.status)
+        self.client.wait_for_resource_deletion(aggregate['id'])
+
+    @test.attr(type='gate')
+    def test_aggregate_create_delete_with_az(self):
+        # Create and delete an aggregate.
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        az_name = data_utils.rand_name(self.az_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name, az_name)
+        self.assertEqual(201, resp.status)
+        self.assertEqual(aggregate_name, aggregate['name'])
+        self.assertEqual(az_name, aggregate['availability_zone'])
+
+        resp, _ = self.client.delete_aggregate(aggregate['id'])
+        self.assertEqual(204, resp.status)
+        self.client.wait_for_resource_deletion(aggregate['id'])
+
+    @test.attr(type='gate')
+    def test_aggregate_create_verify_entry_in_list(self):
+        # Create an aggregate and ensure it is listed.
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        resp, aggregates = self.client.list_aggregates()
+        self.assertEqual(200, resp.status)
+        self.assertIn((aggregate['id'], aggregate['availability_zone']),
+                      map(lambda x: (x['id'], x['availability_zone']),
+                          aggregates))
+
+    @test.attr(type='gate')
+    def test_aggregate_create_update_metadata_get_details(self):
+        # Create an aggregate and ensure its details are returned.
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        resp, body = self.client.get_aggregate(aggregate['id'])
+        self.assertEqual(200, resp.status)
+        self.assertEqual(aggregate['name'], body['name'])
+        self.assertEqual(aggregate['availability_zone'],
+                         body['availability_zone'])
+        self.assertEqual({}, body["metadata"])
+
+        # set the metadata of the aggregate
+        meta = {"key": "value"}
+        resp, body = self.client.set_metadata(aggregate['id'], meta)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(meta, body["metadata"])
+
+        # verify the metadata has been set
+        resp, body = self.client.get_aggregate(aggregate['id'])
+        self.assertEqual(200, resp.status)
+        self.assertEqual(meta, body["metadata"])
+
+    @test.attr(type='gate')
+    def test_aggregate_create_update_with_az(self):
+        # Update an aggregate and ensure properties are updated correctly
+        self.useFixture(fixtures.LockFixture('availability_zone'))
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        az_name = data_utils.rand_name(self.az_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name, az_name)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        self.assertEqual(201, resp.status)
+        self.assertEqual(aggregate_name, aggregate['name'])
+        self.assertEqual(az_name, aggregate['availability_zone'])
+        self.assertIsNotNone(aggregate['id'])
+
+        aggregate_id = aggregate['id']
+        new_aggregate_name = aggregate_name + '_new'
+        new_az_name = az_name + '_new'
+
+        resp, resp_aggregate = self.client.update_aggregate(aggregate_id,
+                                                            new_aggregate_name,
+                                                            new_az_name)
+        self.assertEqual(200, resp.status)
+        self.assertEqual(new_aggregate_name, resp_aggregate['name'])
+        self.assertEqual(new_az_name, resp_aggregate['availability_zone'])
+
+        resp, aggregates = self.client.list_aggregates()
+        self.assertEqual(200, resp.status)
+        self.assertIn((aggregate_id, new_aggregate_name, new_az_name),
+                      map(lambda x:
+                         (x['id'], x['name'], x['availability_zone']),
+                          aggregates))
+
+    @test.attr(type='gate')
+    def test_aggregate_add_remove_host(self):
+        # Add an host to the given aggregate and remove.
+        self.useFixture(fixtures.LockFixture('availability_zone'))
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        resp, body = self.client.add_host(aggregate['id'], self.host)
+        self.assertEqual(202, resp.status)
+        self.assertEqual(aggregate_name, body['name'])
+        self.assertEqual(aggregate['availability_zone'],
+                         body['availability_zone'])
+        self.assertIn(self.host, body['hosts'])
+
+        resp, body = self.client.remove_host(aggregate['id'], self.host)
+        self.assertEqual(202, resp.status)
+        self.assertEqual(aggregate_name, body['name'])
+        self.assertEqual(aggregate['availability_zone'],
+                         body['availability_zone'])
+        self.assertNotIn(self.host, body['hosts'])
+
+    @test.attr(type='gate')
+    def test_aggregate_add_host_list(self):
+        # Add an host to the given aggregate and list.
+        self.useFixture(fixtures.LockFixture('availability_zone'))
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+        self.client.add_host(aggregate['id'], self.host)
+        self.addCleanup(self.client.remove_host, aggregate['id'], self.host)
+
+        resp, aggregates = self.client.list_aggregates()
+        aggs = filter(lambda x: x['id'] == aggregate['id'], aggregates)
+        self.assertEqual(1, len(aggs))
+        agg = aggs[0]
+        self.assertEqual(aggregate_name, agg['name'])
+        self.assertEqual(None, agg['availability_zone'])
+        self.assertIn(self.host, agg['hosts'])
+
+    @test.attr(type='gate')
+    def test_aggregate_add_host_get_details(self):
+        # Add an host to the given aggregate and get details.
+        self.useFixture(fixtures.LockFixture('availability_zone'))
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+        self.client.add_host(aggregate['id'], self.host)
+        self.addCleanup(self.client.remove_host, aggregate['id'], self.host)
+
+        resp, body = self.client.get_aggregate(aggregate['id'])
+        self.assertEqual(aggregate_name, body['name'])
+        self.assertEqual(None, body['availability_zone'])
+        self.assertIn(self.host, body['hosts'])
+
+    @test.attr(type='gate')
+    def test_aggregate_add_host_create_server_with_az(self):
+        # Add an host to the given aggregate and create a server.
+        self.useFixture(fixtures.LockFixture('availability_zone'))
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        az_name = data_utils.rand_name(self.az_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name, az_name)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+        self.client.add_host(aggregate['id'], self.host)
+        self.addCleanup(self.client.remove_host, aggregate['id'], self.host)
+        server_name = data_utils.rand_name('test_server_')
+        admin_servers_client = self.servers_admin_client
+        resp, server = self.create_test_server(name=server_name,
+                                               availability_zone=az_name,
+                                               wait_until='ACTIVE')
+        resp, body = admin_servers_client.get_server(server['id'])
+        self.assertEqual(self.host, body[self._host_key])
+
+
+class AggregatesAdminV3TestXML(AggregatesAdminV3TestJSON):
+    _host_key = (
+        '{http://docs.openstack.org/compute/ext/'
+        'extended_server_attributes/api/v3}host')
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/admin/test_aggregates_negative.py b/tempest/api/compute/v3/admin/test_aggregates_negative.py
new file mode 100644
index 0000000..87eadce
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_aggregates_negative.py
@@ -0,0 +1,196 @@
+# 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.compute import base
+from tempest.common import tempest_fixtures as fixtures
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest import test
+
+
+class AggregatesAdminNegativeV3TestJSON(base.BaseV3ComputeAdminTest):
+
+    """
+    Tests Aggregates API that require admin privileges
+    """
+
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(AggregatesAdminNegativeV3TestJSON, cls).setUpClass()
+        cls.client = cls.aggregates_admin_client
+        cls.user_client = cls.aggregates_client
+        cls.aggregate_name_prefix = 'test_aggregate_'
+        cls.az_name_prefix = 'test_az_'
+
+        resp, hosts_all = cls.hosts_admin_client.list_hosts()
+        hosts = map(lambda x: x['host_name'],
+                    filter(lambda y: y['service'] == 'compute', hosts_all))
+        cls.host = hosts[0]
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_create_as_user(self):
+        # Regular user is not allowed to create an aggregate.
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        self.assertRaises(exceptions.Unauthorized,
+                          self.user_client.create_aggregate,
+                          aggregate_name)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_create_aggregate_name_length_less_than_1(self):
+        # the length of aggregate name should >= 1 and <=255
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_aggregate,
+                          '')
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_create_aggregate_name_length_exceeds_255(self):
+        # the length of aggregate name should >= 1 and <=255
+        aggregate_name = 'a' * 256
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.create_aggregate,
+                          aggregate_name)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_create_with_existent_aggregate_name(self):
+        # creating an aggregate with existent aggregate name is forbidden
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.assertEqual(201, resp.status)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        self.assertRaises(exceptions.Conflict,
+                          self.client.create_aggregate,
+                          aggregate_name)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_delete_as_user(self):
+        # Regular user is not allowed to delete an aggregate.
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.assertEqual(201, resp.status)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.user_client.delete_aggregate,
+                          aggregate['id'])
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_list_as_user(self):
+        # Regular user is not allowed to list aggregates.
+        self.assertRaises(exceptions.Unauthorized,
+                          self.user_client.list_aggregates)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_get_details_as_user(self):
+        # Regular user is not allowed to get aggregate details.
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.assertEqual(201, resp.status)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.user_client.get_aggregate,
+                          aggregate['id'])
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_delete_with_invalid_id(self):
+        # Delete an aggregate with invalid id should raise exceptions.
+        self.assertRaises(exceptions.NotFound,
+                          self.client.delete_aggregate, -1)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_get_details_with_invalid_id(self):
+        # Get aggregate details with invalid id should raise exceptions.
+        self.assertRaises(exceptions.NotFound,
+                          self.client.get_aggregate, -1)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_add_non_exist_host(self):
+        # Adding a non-exist host to an aggregate should raise exceptions.
+        resp, hosts_all = self.hosts_admin_client.list_hosts()
+        hosts = map(lambda x: x['host_name'], hosts_all)
+        while True:
+            non_exist_host = data_utils.rand_name('nonexist_host_')
+            if non_exist_host not in hosts:
+                break
+
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        self.assertRaises(exceptions.NotFound, self.client.add_host,
+                          aggregate['id'], non_exist_host)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_add_host_as_user(self):
+        # Regular user is not allowed to add a host to an aggregate.
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.assertEqual(201, resp.status)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.user_client.add_host,
+                          aggregate['id'], self.host)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_add_existent_host(self):
+        self.useFixture(fixtures.LockFixture('availability_zone'))
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.assertEqual(201, resp.status)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        resp, body = self.client.add_host(aggregate['id'], self.host)
+        self.assertEqual(202, resp.status)
+        self.addCleanup(self.client.remove_host, aggregate['id'], self.host)
+
+        self.assertRaises(exceptions.Conflict, self.client.add_host,
+                          aggregate['id'], self.host)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_remove_host_as_user(self):
+        # Regular user is not allowed to remove a host from an aggregate.
+        self.useFixture(fixtures.LockFixture('availability_zone'))
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.assertEqual(201, resp.status)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+        resp, body = self.client.add_host(aggregate['id'], self.host)
+        self.assertEqual(202, resp.status)
+        self.addCleanup(self.client.remove_host, aggregate['id'], self.host)
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.user_client.remove_host,
+                          aggregate['id'], self.host)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_aggregate_remove_nonexistent_host(self):
+        non_exist_host = data_utils.rand_name('nonexist_host_')
+        aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
+        resp, aggregate = self.client.create_aggregate(aggregate_name)
+        self.assertEqual(201, resp.status)
+        self.addCleanup(self.client.delete_aggregate, aggregate['id'])
+
+        self.assertRaises(exceptions.NotFound, self.client.remove_host,
+                          aggregate['id'], non_exist_host)
+
+
+class AggregatesAdminNegativeV3TestXML(AggregatesAdminNegativeV3TestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/admin/test_hosts.py b/tempest/api/compute/v3/admin/test_hosts.py
new file mode 100644
index 0000000..896d6a7
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_hosts.py
@@ -0,0 +1,94 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.
+
+from tempest.api.compute import base
+from tempest.common import tempest_fixtures as fixtures
+from tempest import test
+
+
+class HostsAdminV3TestJSON(base.BaseV3ComputeAdminTest):
+
+    """
+    Tests hosts API using admin privileges.
+    """
+
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(HostsAdminV3TestJSON, cls).setUpClass()
+        cls.client = cls.hosts_admin_client
+
+    @test.attr(type='gate')
+    def test_list_hosts(self):
+        resp, hosts = self.client.list_hosts()
+        self.assertEqual(200, resp.status)
+        self.assertTrue(len(hosts) >= 2, str(hosts))
+
+    @test.attr(type='gate')
+    def test_list_hosts_with_zone(self):
+        self.useFixture(fixtures.LockFixture('availability_zone'))
+        resp, hosts = self.client.list_hosts()
+        host = hosts[0]
+        zone_name = host['zone']
+        params = {'zone': zone_name}
+        resp, hosts = self.client.list_hosts(params)
+        self.assertEqual(200, resp.status)
+        self.assertTrue(len(hosts) >= 1)
+        self.assertIn(host, hosts)
+
+    @test.attr(type='gate')
+    def test_list_hosts_with_a_blank_zone(self):
+        # If send the request with a blank zone, the request will be successful
+        # and it will return all the hosts list
+        params = {'zone': ''}
+        resp, hosts = self.client.list_hosts(params)
+        self.assertNotEqual(0, len(hosts))
+        self.assertEqual(200, resp.status)
+
+    @test.attr(type='gate')
+    def test_list_hosts_with_nonexistent_zone(self):
+        # If send the request with a nonexistent zone, the request will be
+        # successful and no hosts will be retured
+        params = {'zone': 'xxx'}
+        resp, hosts = self.client.list_hosts(params)
+        self.assertEqual(0, len(hosts))
+        self.assertEqual(200, resp.status)
+
+    @test.attr(type='gate')
+    def test_show_host_detail(self):
+        resp, hosts = self.client.list_hosts()
+        self.assertEqual(200, resp.status)
+
+        hosts = [host for host in hosts if host['service'] == 'compute']
+        self.assertTrue(len(hosts) >= 1)
+
+        for host in hosts:
+            hostname = host['host_name']
+            resp, resources = self.client.show_host_detail(hostname)
+            self.assertEqual(200, resp.status)
+            self.assertTrue(len(resources) >= 1)
+            host_resource = resources[0]['resource']
+            self.assertIsNotNone(host_resource)
+            self.assertIsNotNone(host_resource['cpu'])
+            self.assertIsNotNone(host_resource['disk_gb'])
+            self.assertIsNotNone(host_resource['memory_mb'])
+            self.assertIsNotNone(host_resource['project'])
+            self.assertEqual(hostname, host_resource['host'])
+
+
+class HostsAdminV3TestXML(HostsAdminV3TestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/admin/test_hosts_negative.py b/tempest/api/compute/v3/admin/test_hosts_negative.py
new file mode 100644
index 0000000..755fa2b
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_hosts_negative.py
@@ -0,0 +1,174 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Huawei Technologies Co.,LTD.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.api.compute import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest import test
+
+
+class HostsAdminNegativeV3TestJSON(base.BaseV3ComputeAdminTest):
+
+    """
+    Tests hosts API using admin privileges.
+    """
+
+    _interface = 'json'
+
+    @classmethod
+    def setUpClass(cls):
+        super(HostsAdminNegativeV3TestJSON, cls).setUpClass()
+        cls.client = cls.hosts_admin_client
+        cls.non_admin_client = cls.hosts_client
+
+    def _get_host_name(self):
+        resp, hosts = self.client.list_hosts()
+        self.assertEqual(200, resp.status)
+        self.assertTrue(len(hosts) >= 1)
+        hostname = hosts[0]['host_name']
+        return hostname
+
+    @test.attr(type=['negative', 'gate'])
+    def test_list_hosts_with_non_admin_user(self):
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.list_hosts)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_show_host_detail_with_nonexistent_hostname(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+        self.assertRaises(exceptions.NotFound,
+                          self.client.show_host_detail, nonexitent_hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_show_host_detail_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.show_host_detail,
+                          hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_update_host_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.update_host,
+                          hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_update_host_with_extra_param(self):
+        # only 'status' and 'maintenance_mode' are the valid params.
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_host,
+                          hostname,
+                          status='enable',
+                          maintenance_mode='enable',
+                          param='XXX')
+
+    @test.attr(type=['negative', 'gate'])
+    def test_update_host_with_invalid_status(self):
+        # 'status' can only be 'enable' or 'disable'
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_host,
+                          hostname,
+                          status='invalid',
+                          maintenance_mode='enable')
+
+    @test.attr(type=['negative', 'gate'])
+    def test_update_host_with_invalid_maintenance_mode(self):
+        # 'maintenance_mode' can only be 'enable' or 'disable'
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_host,
+                          hostname,
+                          status='enable',
+                          maintenance_mode='invalid')
+
+    @test.attr(type=['negative', 'gate'])
+    def test_update_host_without_param(self):
+        # 'status' or 'maintenance_mode' needed for host update
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.update_host,
+                          hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_update_nonexistent_host(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+        self.assertRaises(exceptions.NotFound,
+                          self.client.update_host,
+                          nonexitent_hostname,
+                          status='enable',
+                          maintenance_mode='enable')
+
+    @test.attr(type=['negative', 'gate'])
+    def test_startup_nonexistent_host(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+        self.assertRaises(exceptions.NotFound,
+                          self.client.startup_host,
+                          nonexitent_hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_startup_host_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.startup_host,
+                          hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_shutdown_nonexistent_host(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+        self.assertRaises(exceptions.NotFound,
+                          self.client.shutdown_host,
+                          nonexitent_hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_shutdown_host_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.shutdown_host,
+                          hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_reboot_nonexistent_host(self):
+        nonexitent_hostname = data_utils.rand_name('rand_hostname')
+
+        self.assertRaises(exceptions.NotFound,
+                          self.client.reboot_host,
+                          nonexitent_hostname)
+
+    @test.attr(type=['negative', 'gate'])
+    def test_reboot_host_with_non_admin_user(self):
+        hostname = self._get_host_name()
+
+        self.assertRaises(exceptions.Unauthorized,
+                          self.non_admin_client.reboot_host,
+                          hostname)
+
+
+class HostsAdminNegativeV3TestXML(HostsAdminNegativeV3TestJSON):
+    _interface = 'xml'
diff --git a/tempest/api/compute/v3/test_live_block_migration.py b/tempest/api/compute/v3/test_live_block_migration.py
new file mode 100644
index 0000000..3de50c8
--- /dev/null
+++ b/tempest/api/compute/v3/test_live_block_migration.py
@@ -0,0 +1,173 @@
+# 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 random
+import string
+
+import testtools
+
+from tempest.api.compute import base
+from tempest import config
+from tempest import exceptions
+from tempest.test import attr
+
+
+class LiveBlockMigrationV3TestJSON(base.BaseV3ComputeAdminTest):
+    _host_key = 'os-extended-server-attributes:host'
+    _interface = 'json'
+
+    CONF = config.CONF
+
+    @classmethod
+    def setUpClass(cls):
+        super(LiveBlockMigrationV3TestJSON, cls).setUpClass()
+
+        cls.admin_hosts_client = cls.hosts_admin_client
+        cls.admin_servers_client = cls.servers_admin_client
+
+        cls.created_server_ids = []
+
+    def _get_compute_hostnames(self):
+        _resp, body = self.admin_hosts_client.list_hosts()
+        return [
+            host_record['host_name']
+            for host_record in body
+            if host_record['service'] == 'compute'
+        ]
+
+    def _get_server_details(self, server_id):
+        _resp, body = self.admin_servers_client.get_server(server_id)
+        return body
+
+    def _get_host_for_server(self, server_id):
+        return self._get_server_details(server_id)[self._host_key]
+
+    def _migrate_server_to(self, server_id, dest_host):
+        _resp, body = self.admin_servers_client.live_migrate_server(
+            server_id, dest_host,
+            self.config.compute_feature_enabled.
+            block_migration_for_live_migration)
+        return body
+
+    def _get_host_other_than(self, host):
+        for target_host in self._get_compute_hostnames():
+            if host != target_host:
+                return target_host
+
+    def _get_non_existing_host_name(self):
+        random_name = ''.join(
+            random.choice(string.ascii_uppercase) for x in range(20))
+
+        self.assertNotIn(random_name, self._get_compute_hostnames())
+
+        return random_name
+
+    def _get_server_status(self, server_id):
+        return self._get_server_details(server_id)['status']
+
+    def _get_an_active_server(self):
+        for server_id in self.created_server_ids:
+            if 'ACTIVE' == self._get_server_status(server_id):
+                return server_id
+        else:
+            _, server = self.create_test_server(wait_until="ACTIVE")
+            server_id = server['id']
+            self.password = server['admin_password']
+            self.password = 'password'
+            self.created_server_ids.append(server_id)
+            return server_id
+
+    def _volume_clean_up(self, server_id, volume_id):
+        resp, body = self.volumes_client.get_volume(volume_id)
+        if body['status'] == 'in-use':
+            self.servers_client.detach_volume(server_id, volume_id)
+            self.volumes_client.wait_for_volume_status(volume_id, 'available')
+        self.volumes_client.delete_volume(volume_id)
+
+    @testtools.skipIf(not CONF.compute_feature_enabled.live_migration,
+                      'Live migration not available')
+    @attr(type='gate')
+    def test_live_block_migration(self):
+        # Live block migrate an instance to another host
+        if len(self._get_compute_hostnames()) < 2:
+            raise self.skipTest(
+                "Less than 2 compute nodes, skipping migration test.")
+        server_id = self._get_an_active_server()
+        actual_host = self._get_host_for_server(server_id)
+        target_host = self._get_host_other_than(actual_host)
+        self._migrate_server_to(server_id, target_host)
+        self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+        self.assertEqual(target_host, self._get_host_for_server(server_id))
+
+    @testtools.skipIf(not CONF.compute_feature_enabled.live_migration,
+                      'Live migration not available')
+    @attr(type='gate')
+    def test_invalid_host_for_migration(self):
+        # Migrating to an invalid host should not change the status
+        server_id = self._get_an_active_server()
+        target_host = self._get_non_existing_host_name()
+
+        self.assertRaises(exceptions.BadRequest, self._migrate_server_to,
+                          server_id, target_host)
+        self.assertEqual('ACTIVE', self._get_server_status(server_id))
+
+    @testtools.skipIf(not CONF.compute_feature_enabled.live_migration or not
+                      CONF.compute_feature_enabled.
+                      block_migration_for_live_migration,
+                      'Block Live migration not available')
+    @testtools.skipIf(not CONF.compute_feature_enabled.
+                      block_migrate_cinder_iscsi,
+                      'Block Live migration not configured for iSCSI')
+    @attr(type='gate')
+    def test_iscsi_volume(self):
+        # Live block migrate an instance to another host
+        if len(self._get_compute_hostnames()) < 2:
+            raise self.skipTest(
+                "Less than 2 compute nodes, skipping migration test.")
+        server_id = self._get_an_active_server()
+        actual_host = self._get_host_for_server(server_id)
+        target_host = self._get_host_other_than(actual_host)
+
+        resp, volume = self.volumes_client.create_volume(1,
+                                                         display_name='test')
+
+        self.volumes_client.wait_for_volume_status(volume['id'],
+                                                   'available')
+        self.addCleanup(self._volume_clean_up, server_id, volume['id'])
+
+        # Attach the volume to the server
+        self.servers_client.attach_volume(server_id, volume['id'],
+                                          device='/dev/xvdb')
+        self.volumes_client.wait_for_volume_status(volume['id'], 'in-use')
+
+        self._migrate_server_to(server_id, target_host)
+        self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+        self.assertEqual(target_host, self._get_host_for_server(server_id))
+
+    @classmethod
+    def tearDownClass(cls):
+        for server_id in cls.created_server_ids:
+            cls.servers_client.delete_server(server_id)
+
+        super(LiveBlockMigrationV3TestJSON, cls).tearDownClass()
+
+
+class LiveBlockMigrationV3TestXML(LiveBlockMigrationV3TestJSON):
+    _host_key = (
+        '{http://docs.openstack.org/compute/ext/'
+        'extended_server_attributes/api/v3}host')
+    _interface = 'xml'
diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py
index f3ef99f..b6c03e0 100644
--- a/tempest/api/orchestration/base.py
+++ b/tempest/api/orchestration/base.py
@@ -12,8 +12,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import time
-
 from tempest import clients
 from tempest.common.utils import data_utils
 from tempest.openstack.common import log as logging
@@ -118,21 +116,6 @@
         cls.clear_keypairs()
         super(BaseOrchestrationTest, cls).tearDownClass()
 
-    def wait_for(self, condition):
-        """Repeatedly calls condition() until a timeout."""
-        start_time = int(time.time())
-        while True:
-            try:
-                condition()
-            except Exception:
-                pass
-            else:
-                return
-            if int(time.time()) - start_time >= self.build_timeout:
-                condition()
-                return
-            time.sleep(self.build_interval)
-
     @staticmethod
     def stack_output(stack, output_key):
         """Return a stack output value for a give key."""
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index d63fd8b..643d021 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -15,8 +15,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import time
-
 from tempest import clients
 from tempest.openstack.common import log as logging
 import tempest.test
@@ -107,21 +105,6 @@
             except Exception:
                 pass
 
-    def wait_for(self, condition):
-        """Repeatedly calls condition() until a timeout."""
-        start_time = int(time.time())
-        while True:
-            try:
-                condition()
-            except Exception:
-                pass
-            else:
-                return
-            if int(time.time()) - start_time >= self.build_timeout:
-                condition()
-                return
-            time.sleep(self.build_interval)
-
 
 class BaseVolumeV1Test(BaseVolumeTest):
     @classmethod
diff --git a/tempest/cli/simple_read_only/test_ceilometer.py b/tempest/cli/simple_read_only/test_ceilometer.py
index 7f2864f..8bdd633 100644
--- a/tempest/cli/simple_read_only/test_ceilometer.py
+++ b/tempest/cli/simple_read_only/test_ceilometer.py
@@ -49,3 +49,6 @@
 
     def test_ceilometermeter_alarm_list(self):
         self.ceilometer('alarm-list')
+
+    def test_ceilometer_version(self):
+        self.ceilometer('', flags='--version')
diff --git a/tempest/clients.py b/tempest/clients.py
index 93860c1..4fe81e3 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -50,6 +50,8 @@
     TenantUsagesClientJSON
 from tempest.services.compute.json.volumes_extensions_client import \
     VolumesExtensionsClientJSON
+from tempest.services.compute.v3.json.aggregates_client import \
+    AggregatesV3ClientJSON
 from tempest.services.compute.v3.json.availability_zone_client import \
     AvailabilityZoneV3ClientJSON
 from tempest.services.compute.v3.json.certificates_client import \
@@ -57,6 +59,7 @@
 from tempest.services.compute.v3.json.extensions_client import \
     ExtensionsV3ClientJSON
 from tempest.services.compute.v3.json.flavors_client import FlavorsV3ClientJSON
+from tempest.services.compute.v3.json.hosts_client import HostsV3ClientJSON
 from tempest.services.compute.v3.json.hypervisor_client import \
     HypervisorV3ClientJSON
 from tempest.services.compute.v3.json.interfaces_client import \
@@ -69,6 +72,8 @@
     ServicesV3ClientJSON
 from tempest.services.compute.v3.json.tenant_usages_client import \
     TenantUsagesV3ClientJSON
+from tempest.services.compute.v3.xml.aggregates_client import \
+    AggregatesV3ClientXML
 from tempest.services.compute.v3.xml.availability_zone_client import \
     AvailabilityZoneV3ClientXML
 from tempest.services.compute.v3.xml.certificates_client import \
@@ -76,6 +81,7 @@
 from tempest.services.compute.v3.xml.extensions_client import \
     ExtensionsV3ClientXML
 from tempest.services.compute.v3.xml.flavors_client import FlavorsV3ClientXML
+from tempest.services.compute.v3.xml.hosts_client import HostsV3ClientXML
 from tempest.services.compute.v3.xml.hypervisor_client import \
     HypervisorV3ClientXML
 from tempest.services.compute.v3.xml.interfaces_client import \
@@ -255,6 +261,7 @@
                 *client_args)
             self.services_v3_client = ServicesV3ClientXML(*client_args)
             self.service_client = ServiceClientXML(*client_args)
+            self.aggregates_v3_client = AggregatesV3ClientXML(*client_args)
             self.aggregates_client = AggregatesClientXML(*client_args)
             self.services_client = ServicesClientXML(*client_args)
             self.tenant_usages_v3_client = TenantUsagesV3ClientXML(
@@ -272,6 +279,7 @@
             self.volume_hosts_client = VolumeHostsClientXML(*client_args)
             self.volumes_extension_client = VolumeExtensionClientXML(
                 *client_args)
+            self.hosts_v3_client = HostsV3ClientXML(*client_args)
 
             if client_args_v3_auth:
                 self.servers_client_v3_auth = ServersClientXML(
@@ -314,6 +322,7 @@
                 *client_args)
             self.services_v3_client = ServicesV3ClientJSON(*client_args)
             self.service_client = ServiceClientJSON(*client_args)
+            self.aggregates_v3_client = AggregatesV3ClientJSON(*client_args)
             self.aggregates_client = AggregatesClientJSON(*client_args)
             self.services_client = ServicesClientJSON(*client_args)
             self.tenant_usages_v3_client = TenantUsagesV3ClientJSON(
@@ -331,6 +340,7 @@
             self.volume_hosts_client = VolumeHostsClientJSON(*client_args)
             self.volumes_extension_client = VolumeExtensionClientJSON(
                 *client_args)
+            self.hosts_v3_client = HostsV3ClientJSON(*client_args)
 
             if client_args_v3_auth:
                 self.servers_client_v3_auth = ServersClientJSON(
diff --git a/tempest/services/compute/v3/json/aggregates_client.py b/tempest/services/compute/v3/json/aggregates_client.py
new file mode 100644
index 0000000..8ca2770
--- /dev/null
+++ b/tempest/services/compute/v3/json/aggregates_client.py
@@ -0,0 +1,111 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation.
+# 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 json
+
+from tempest.common.rest_client import RestClient
+from tempest import exceptions
+
+
+class AggregatesV3ClientJSON(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(AggregatesV3ClientJSON, self).__init__(config, username,
+                                                     password, auth_url,
+                                                     tenant_name)
+        self.service = self.config.compute.catalog_v3_type
+
+    def list_aggregates(self):
+        """Get aggregate list."""
+        resp, body = self.get("os-aggregates")
+        body = json.loads(body)
+        return resp, body['aggregates']
+
+    def get_aggregate(self, aggregate_id):
+        """Get details of the given aggregate."""
+        resp, body = self.get("os-aggregates/%s" % str(aggregate_id))
+        body = json.loads(body)
+        return resp, body['aggregate']
+
+    def create_aggregate(self, name, availability_zone=None):
+        """Creates a new aggregate."""
+        post_body = {
+            'name': name,
+            'availability_zone': availability_zone,
+        }
+        post_body = json.dumps({'aggregate': post_body})
+        resp, body = self.post('os-aggregates', post_body, self.headers)
+
+        body = json.loads(body)
+        return resp, body['aggregate']
+
+    def update_aggregate(self, aggregate_id, name, availability_zone=None):
+        """Update a aggregate."""
+        put_body = {
+            'name': name,
+            'availability_zone': availability_zone
+        }
+        put_body = json.dumps({'aggregate': put_body})
+        resp, body = self.put('os-aggregates/%s' % str(aggregate_id),
+                              put_body, self.headers)
+
+        body = json.loads(body)
+        return resp, body['aggregate']
+
+    def delete_aggregate(self, aggregate_id):
+        """Deletes the given aggregate."""
+        return self.delete("os-aggregates/%s" % str(aggregate_id))
+
+    def is_resource_deleted(self, id):
+        try:
+            self.get_aggregate(id)
+        except exceptions.NotFound:
+            return True
+        return False
+
+    def add_host(self, aggregate_id, host):
+        """Adds a host to the given aggregate."""
+        post_body = {
+            'host': host,
+        }
+        post_body = json.dumps({'add_host': post_body})
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               post_body, self.headers)
+        body = json.loads(body)
+        return resp, body['aggregate']
+
+    def remove_host(self, aggregate_id, host):
+        """Removes a host from the given aggregate."""
+        post_body = {
+            'host': host,
+        }
+        post_body = json.dumps({'remove_host': post_body})
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               post_body, self.headers)
+        body = json.loads(body)
+        return resp, body['aggregate']
+
+    def set_metadata(self, aggregate_id, meta):
+        """Replaces the aggregate's existing metadata with new metadata."""
+        post_body = {
+            'metadata': meta,
+        }
+        post_body = json.dumps({'set_metadata': post_body})
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               post_body, self.headers)
+        body = json.loads(body)
+        return resp, body['aggregate']
diff --git a/tempest/services/compute/v3/json/hosts_client.py b/tempest/services/compute/v3/json/hosts_client.py
new file mode 100644
index 0000000..85cc34f
--- /dev/null
+++ b/tempest/services/compute/v3/json/hosts_client.py
@@ -0,0 +1,82 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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 json
+import urllib
+
+from tempest.common.rest_client import RestClient
+
+
+class HostsV3ClientJSON(RestClient):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(HostsV3ClientJSON, self).__init__(config, username, password,
+                                                auth_url, tenant_name)
+        self.service = self.config.compute.catalog_v3_type
+
+    def list_hosts(self, params=None):
+        """Lists all hosts."""
+
+        url = 'os-hosts'
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['hosts']
+
+    def show_host_detail(self, hostname):
+        """Show detail information for the host."""
+
+        resp, body = self.get("os-hosts/%s" % str(hostname))
+        body = json.loads(body)
+        return resp, body['host']
+
+    def update_host(self, hostname, **kwargs):
+        """Update a host."""
+
+        request_body = {
+            'status': None,
+            'maintenance_mode': None,
+        }
+        request_body.update(**kwargs)
+        request_body = json.dumps({'host': request_body})
+
+        resp, body = self.put("os-hosts/%s" % str(hostname), request_body,
+                              self.headers)
+        body = json.loads(body)
+        return resp, body
+
+    def startup_host(self, hostname):
+        """Startup a host."""
+
+        resp, body = self.get("os-hosts/%s/startup" % str(hostname))
+        body = json.loads(body)
+        return resp, body['host']
+
+    def shutdown_host(self, hostname):
+        """Shutdown a host."""
+
+        resp, body = self.get("os-hosts/%s/shutdown" % str(hostname))
+        body = json.loads(body)
+        return resp, body['host']
+
+    def reboot_host(self, hostname):
+        """reboot a host."""
+
+        resp, body = self.get("os-hosts/%s/reboot" % str(hostname))
+        body = json.loads(body)
+        return resp, body['host']
diff --git a/tempest/services/compute/v3/xml/aggregates_client.py b/tempest/services/compute/v3/xml/aggregates_client.py
new file mode 100644
index 0000000..7563fd6
--- /dev/null
+++ b/tempest/services/compute/v3/xml/aggregates_client.py
@@ -0,0 +1,130 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation.
+# 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 lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest import exceptions
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
+from tempest.services.compute.xml.common import xml_to_json
+
+
+class AggregatesV3ClientXML(RestClientXML):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(AggregatesV3ClientXML, self).__init__(config, username, password,
+                                                    auth_url, tenant_name)
+        self.service = self.config.compute.catalog_v3_type
+
+    def _format_aggregate(self, g):
+        agg = xml_to_json(g)
+        aggregate = {}
+        for key, value in agg.items():
+            if key == 'hosts':
+                aggregate['hosts'] = []
+                for k, v in value.items():
+                    aggregate['hosts'].append(v)
+            elif key == 'availability_zone':
+                aggregate[key] = None if value == 'None' else value
+            else:
+                aggregate[key] = value
+        return aggregate
+
+    def _parse_array(self, node):
+        return [self._format_aggregate(x) for x in node]
+
+    def list_aggregates(self):
+        """Get aggregate list."""
+        resp, body = self.get("os-aggregates", self.headers)
+        aggregates = self._parse_array(etree.fromstring(body))
+        return resp, aggregates
+
+    def get_aggregate(self, aggregate_id):
+        """Get details of the given aggregate."""
+        resp, body = self.get("os-aggregates/%s" % str(aggregate_id),
+                              self.headers)
+        aggregate = self._format_aggregate(etree.fromstring(body))
+        return resp, aggregate
+
+    def create_aggregate(self, name, availability_zone=None):
+        """Creates a new aggregate."""
+        post_body = Element("aggregate",
+                            name=name,
+                            availability_zone=availability_zone)
+        resp, body = self.post('os-aggregates',
+                               str(Document(post_body)),
+                               self.headers)
+        aggregate = self._format_aggregate(etree.fromstring(body))
+        return resp, aggregate
+
+    def update_aggregate(self, aggregate_id, name, availability_zone=None):
+        """Update a aggregate."""
+        put_body = Element("aggregate",
+                           name=name,
+                           availability_zone=availability_zone)
+        resp, body = self.put('os-aggregates/%s' % str(aggregate_id),
+                              str(Document(put_body)),
+                              self.headers)
+        aggregate = self._format_aggregate(etree.fromstring(body))
+        return resp, aggregate
+
+    def delete_aggregate(self, aggregate_id):
+        """Deletes the given aggregate."""
+        return self.delete("os-aggregates/%s" % str(aggregate_id),
+                           self.headers)
+
+    def is_resource_deleted(self, id):
+        try:
+            self.get_aggregate(id)
+        except exceptions.NotFound:
+            return True
+        return False
+
+    def add_host(self, aggregate_id, host):
+        """Adds a host to the given aggregate."""
+        post_body = Element("add_host", host=host)
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               str(Document(post_body)),
+                               self.headers)
+        aggregate = self._format_aggregate(etree.fromstring(body))
+        return resp, aggregate
+
+    def remove_host(self, aggregate_id, host):
+        """Removes a host from the given aggregate."""
+        post_body = Element("remove_host", host=host)
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               str(Document(post_body)),
+                               self.headers)
+        aggregate = self._format_aggregate(etree.fromstring(body))
+        return resp, aggregate
+
+    def set_metadata(self, aggregate_id, meta):
+        """Replaces the aggregate's existing metadata with new metadata."""
+        post_body = Element("set_metadata")
+        metadata = Element("metadata")
+        post_body.append(metadata)
+        for k, v in meta.items():
+            meta = Element(k)
+            meta.append(Text(v))
+            metadata.append(meta)
+        resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
+                               str(Document(post_body)),
+                               self.headers)
+        aggregate = self._format_aggregate(etree.fromstring(body))
+        return resp, aggregate
diff --git a/tempest/services/compute/v3/xml/hosts_client.py b/tempest/services/compute/v3/xml/hosts_client.py
new file mode 100644
index 0000000..82fb076
--- /dev/null
+++ b/tempest/services/compute/v3/xml/hosts_client.py
@@ -0,0 +1,92 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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 urllib
+
+from lxml import etree
+from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import xml_to_json
+
+
+class HostsV3ClientXML(RestClientXML):
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(HostsV3ClientXML, self).__init__(config, username, password,
+                                               auth_url, tenant_name)
+        self.service = self.config.compute.catalog_v3_type
+
+    def list_hosts(self, params=None):
+        """Lists all hosts."""
+
+        url = 'os-hosts'
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url, self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body
+
+    def show_host_detail(self, hostname):
+        """Show detail information for the host."""
+
+        resp, body = self.get("os-hosts/%s" % str(hostname), self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(node)]
+        return resp, body
+
+    def update_host(self, hostname, **kwargs):
+        """Update a host."""
+
+        request_body = Element("host")
+        if kwargs:
+            for k, v in kwargs.iteritems():
+                request_body.append(Element(k, v))
+        resp, body = self.put("os-hosts/%s" % str(hostname),
+                              str(Document(request_body)),
+                              self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body
+
+    def startup_host(self, hostname):
+        """Startup a host."""
+
+        resp, body = self.get("os-hosts/%s/startup" % str(hostname),
+                              self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body
+
+    def shutdown_host(self, hostname):
+        """Shutdown a host."""
+
+        resp, body = self.get("os-hosts/%s/shutdown" % str(hostname),
+                              self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body
+
+    def reboot_host(self, hostname):
+        """Reboot a host."""
+
+        resp, body = self.get("os-hosts/%s/reboot" % str(hostname),
+                              self.headers)
+        node = etree.fromstring(body)
+        body = [xml_to_json(x) for x in node.getchildren()]
+        return resp, body