Merge "Swift formpost cleanup"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 2fdbb7e..70c791b 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -101,14 +101,32 @@
# Options defined in tempest.config
#
-# Catalog type of the baremetal provisioning service. (string
+# Catalog type of the baremetal provisioning service (string
# value)
#catalog_type=baremetal
+# Whether the Ironic nova-compute driver is enabled (boolean
+# value)
+#driver_enabled=false
+
# The endpoint type to use for the baremetal provisioning
-# service. (string value)
+# service (string value)
#endpoint_type=publicURL
+# Timeout for Ironic node to completely provision (integer
+# value)
+#active_timeout=300
+
+# Timeout for association of Nova instance and Ironic node
+# (integer value)
+#association_timeout=10
+
+# Timeout for Ironic power transitions. (integer value)
+#power_timeout=20
+
+# Timeout for unprovisioning an Ironic node. (integer value)
+#unprovision_timeout=20
+
[boto]
@@ -193,7 +211,7 @@
# admin credentials are known. (boolean value)
#allow_tenant_isolation=false
-# Valid secondary image reference to be used in tests. (string
+# Valid primary image reference to be used in tests. (string
# value)
#image_ref={$IMAGE_ID}
@@ -734,7 +752,7 @@
# Timeout in seconds to wait for a stack to build. (integer
# value)
-#build_timeout=600
+#build_timeout=1200
# Instance type for tests. Needs to be big enough for a full
# OS plus the test workload (string value)
@@ -960,6 +978,10 @@
# value)
#disk_format=raw
+# Default size in GB for volumes created by volumes tests
+# (integer value)
+#volume_size=1
+
[volume-feature-enabled]
diff --git a/requirements.txt b/requirements.txt
index 3521df0..e97eece 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-pbr>=0.6,<1.0
+pbr>=0.6,!=0.7,<1.0
anyjson>=0.3.3
httplib2>=0.7.5
jsonschema>=2.0.0,<3.0.0
@@ -13,10 +13,11 @@
python-neutronclient>=2.3.4,<3
python-cinderclient>=1.0.6
python-heatclient>=0.2.3
+python-ironicclient
python-saharaclient>=0.6.0
python-swiftclient>=1.6
testresources>=0.2.4
-keyring>=1.6.1,<2.0,>=2.1
+keyring>=2.1
testrepository>=0.0.18
oslo.config>=1.2.0
six>=1.5.2
diff --git a/tempest/api/compute/admin/test_agents.py b/tempest/api/compute/admin/test_agents.py
new file mode 100644
index 0000000..4808601
--- /dev/null
+++ b/tempest/api/compute/admin/test_agents.py
@@ -0,0 +1,123 @@
+# Copyright 2014 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.utils import data_utils
+from tempest import exceptions
+from tempest.openstack.common import log
+from tempest import test
+
+LOG = log.getLogger(__name__)
+
+
+class AgentsAdminTestJSON(base.BaseV2ComputeAdminTest):
+ """
+ Tests Agents API
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(AgentsAdminTestJSON, cls).setUpClass()
+ cls.client = cls.os_adm.agents_client
+
+ def setUp(self):
+ super(AgentsAdminTestJSON, self).setUp()
+ params = self._param_helper(
+ hypervisor='common', os='linux', architecture='x86_64',
+ version='7.0', url='xxx://xxxx/xxx/xxx',
+ md5hash='add6bb58e139be103324d04d82d8f545')
+ resp, body = self.client.create_agent(**params)
+ self.assertEqual(200, resp.status)
+ self.agent_id = body['agent_id']
+
+ def tearDown(self):
+ try:
+ self.client.delete_agent(self.agent_id)
+ except exceptions.NotFound:
+ pass
+ except Exception:
+ LOG.exception('Exception raised deleting agent %s', self.agent_id)
+ super(AgentsAdminTestJSON, self).tearDown()
+
+ def _param_helper(self, **kwargs):
+ rand_key = 'architecture'
+ if rand_key in kwargs:
+ # NOTE: The rand_name is for avoiding agent conflicts.
+ # If you try to create an agent with the same hypervisor,
+ # os and architecture as an exising agent, Nova will return
+ # an HTTPConflict or HTTPServerError.
+ kwargs[rand_key] = data_utils.rand_name(kwargs[rand_key])
+ return kwargs
+
+ @test.attr(type='gate')
+ def test_create_agent(self):
+ # Create an agent.
+ params = self._param_helper(
+ hypervisor='kvm', os='win', architecture='x86',
+ version='7.0', url='xxx://xxxx/xxx/xxx',
+ md5hash='add6bb58e139be103324d04d82d8f545')
+ resp, body = self.client.create_agent(**params)
+ self.assertEqual(200, resp.status)
+ self.addCleanup(self.client.delete_agent, body['agent_id'])
+ for expected_item, value in params.items():
+ self.assertEqual(value, body[expected_item])
+
+ @test.attr(type='gate')
+ def test_update_agent(self):
+ # Update an agent.
+ params = self._param_helper(
+ version='8.0', url='xxx://xxxx/xxx/xxx2',
+ md5hash='add6bb58e139be103324d04d82d8f547')
+ resp, body = self.client.update_agent(self.agent_id, **params)
+ self.assertEqual(200, resp.status)
+ for expected_item, value in params.items():
+ self.assertEqual(value, body[expected_item])
+
+ @test.attr(type='gate')
+ def test_delete_agent(self):
+ # Delete an agent.
+ resp, _ = self.client.delete_agent(self.agent_id)
+ self.assertEqual(200, resp.status)
+
+ # Verify the list doesn't contain the deleted agent.
+ resp, agents = self.client.list_agents()
+ self.assertEqual(200, resp.status)
+ self.assertNotIn(self.agent_id, map(lambda x: x['agent_id'], agents))
+
+ @test.attr(type='gate')
+ def test_list_agents(self):
+ # List all agents.
+ resp, agents = self.client.list_agents()
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(agents) > 0, 'Cannot get any agents.(%s)' % agents)
+ self.assertIn(self.agent_id, map(lambda x: x['agent_id'], agents))
+
+ @test.attr(type='gate')
+ def test_list_agents_with_filter(self):
+ # List the agent builds by the filter.
+ params = self._param_helper(
+ hypervisor='xen', os='linux', architecture='x86',
+ version='7.0', url='xxx://xxxx/xxx/xxx1',
+ md5hash='add6bb58e139be103324d04d82d8f546')
+ resp, agent_xen = self.client.create_agent(**params)
+ self.assertEqual(200, resp.status)
+ self.addCleanup(self.client.delete_agent, agent_xen['agent_id'])
+
+ agent_id_xen = agent_xen['agent_id']
+ params_filter = {'hypervisor': agent_xen['hypervisor']}
+ resp, agents = self.client.list_agents(params_filter)
+ self.assertEqual(200, resp.status)
+ self.assertTrue(len(agents) > 0, 'Cannot get any agents.(%s)' % agents)
+ self.assertIn(agent_id_xen, map(lambda x: x['agent_id'], agents))
+ self.assertNotIn(self.agent_id, map(lambda x: x['agent_id'], agents))
diff --git a/tempest/api/compute/admin/test_migrations.py b/tempest/api/compute/admin/test_migrations.py
new file mode 100644
index 0000000..514f1fa
--- /dev/null
+++ b/tempest/api/compute/admin/test_migrations.py
@@ -0,0 +1,55 @@
+# Copyright 2014 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 testtools
+
+from tempest.api.compute import base
+from tempest import config
+from tempest import test
+
+CONF = config.CONF
+
+
+class MigrationsAdminTest(base.BaseV2ComputeAdminTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(MigrationsAdminTest, cls).setUpClass()
+ cls.client = cls.os_adm.migrations_client
+
+ @test.attr(type='gate')
+ def test_list_migrations(self):
+ # Admin can get the migrations list
+ resp, _ = self.client.list_migrations()
+ self.assertEqual(200, resp.status)
+
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
+ @test.attr(type='gate')
+ def test_list_migrations_in_flavor_resize_situation(self):
+ # Admin can get the migrations list which contains the resized server
+ resp, server = self.create_test_server(wait_until="ACTIVE")
+ server_id = server['id']
+
+ resp, _ = self.servers_client.resize(server_id, self.flavor_ref_alt)
+ self.assertEqual(202, resp.status)
+ self.servers_client.wait_for_server_status(server_id, 'VERIFY_RESIZE')
+ self.servers_client.confirm_resize(server_id)
+ self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+
+ resp, body = self.client.list_migrations()
+ self.assertEqual(200, resp.status)
+
+ instance_uuids = [x['instance_uuid'] for x in body]
+ self.assertIn(server_id, instance_uuids)
diff --git a/tempest/api/compute/admin/test_quotas.py b/tempest/api/compute/admin/test_quotas.py
index 09c7274..32e0478 100644
--- a/tempest/api/compute/admin/test_quotas.py
+++ b/tempest/api/compute/admin/test_quotas.py
@@ -76,21 +76,38 @@
# TODO(afazekas): merge these test cases
@test.attr(type='gate')
def test_get_updated_quotas(self):
- # Verify that GET shows the updated quota set
+ # Verify that GET shows the updated quota set of tenant
tenant_name = data_utils.rand_name('cpu_quota_tenant_')
tenant_desc = tenant_name + '-desc'
identity_client = self.os_adm.identity_client
_, tenant = identity_client.create_tenant(name=tenant_name,
description=tenant_desc)
tenant_id = tenant['id']
- self.addCleanup(identity_client.delete_tenant,
- tenant_id)
+ self.addCleanup(identity_client.delete_tenant, tenant_id)
- self.adm_client.update_quota_set(tenant_id,
- ram='5120')
+ self.adm_client.update_quota_set(tenant_id, ram='5120')
resp, quota_set = self.adm_client.get_quota_set(tenant_id)
self.assertEqual(200, resp.status)
- self.assertEqual(quota_set['ram'], 5120)
+ self.assertEqual(5120, quota_set['ram'])
+
+ # Verify that GET shows the updated quota set of user
+ user_name = data_utils.rand_name('cpu_quota_user_')
+ password = data_utils.rand_name('password-')
+ email = user_name + '@testmail.tm'
+ _, user = identity_client.create_user(name=user_name,
+ password=password,
+ tenant_id=tenant_id,
+ email=email)
+ user_id = user['id']
+ self.addCleanup(identity_client.delete_user, user_id)
+
+ self.adm_client.update_quota_set(tenant_id,
+ user_id=user_id,
+ ram='2048')
+ resp, quota_set = self.adm_client.get_quota_set(tenant_id,
+ user_id=user_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(2048, quota_set['ram'])
@test.attr(type='gate')
def test_delete_quota(self):
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index ac43869..d1bda83 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -227,12 +227,14 @@
cls.interfaces_client = cls.os.interfaces_client
cls.fixed_ips_client = cls.os.fixed_ips_client
cls.availability_zone_client = cls.os.availability_zone_client
+ cls.agents_client = cls.os.agents_client
cls.aggregates_client = cls.os.aggregates_client
cls.services_client = cls.os.services_client
cls.instance_usages_audit_log_client = \
cls.os.instance_usages_audit_log_client
cls.hypervisor_client = cls.os.hypervisor_client
cls.certificates_client = cls.os.certificates_client
+ cls.migrations_client = cls.os.migrations_client
@classmethod
def create_image_from_server(cls, server_id, **kwargs):
diff --git a/tempest/api/compute/test_authorization.py b/tempest/api/compute/test_authorization.py
index 7f909d7..c87f24e 100644
--- a/tempest/api/compute/test_authorization.py
+++ b/tempest/api/compute/test_authorization.py
@@ -60,7 +60,7 @@
resp, cls.server = cls.client.get_server(server['id'])
name = data_utils.rand_name('image')
- resp, body = cls.client.create_image(server['id'], name)
+ resp, body = cls.images_client.create_image(server['id'], name)
image_id = data_utils.parse_image_id(resp['location'])
cls.images_client.wait_for_image_status(image_id, 'ACTIVE')
resp, cls.image = cls.images_client.get_image(image_id)
diff --git a/tempest/api/compute/v3/admin/test_quotas.py b/tempest/api/compute/v3/admin/test_quotas.py
index 917c115..b70e254 100644
--- a/tempest/api/compute/v3/admin/test_quotas.py
+++ b/tempest/api/compute/v3/admin/test_quotas.py
@@ -94,21 +94,38 @@
# TODO(afazekas): merge these test cases
@test.attr(type='gate')
def test_get_updated_quotas(self):
- # Verify that GET shows the updated quota set
+ # Verify that GET shows the updated quota set of tenant
tenant_name = data_utils.rand_name('cpu_quota_tenant_')
tenant_desc = tenant_name + '-desc'
identity_client = self.os_adm.identity_client
_, tenant = identity_client.create_tenant(name=tenant_name,
description=tenant_desc)
tenant_id = tenant['id']
- self.addCleanup(identity_client.delete_tenant,
- tenant_id)
+ self.addCleanup(identity_client.delete_tenant, tenant_id)
- self.adm_client.update_quota_set(tenant_id,
- ram='5120')
+ self.adm_client.update_quota_set(tenant_id, ram='5120')
resp, quota_set = self.adm_client.get_quota_set(tenant_id)
self.assertEqual(200, resp.status)
- self.assertEqual(quota_set['ram'], 5120)
+ self.assertEqual(5120, quota_set['ram'])
+
+ # Verify that GET shows the updated quota set of user
+ user_name = data_utils.rand_name('cpu_quota_user_')
+ password = data_utils.rand_name('password-')
+ email = user_name + '@testmail.tm'
+ _, user = identity_client.create_user(name=user_name,
+ password=password,
+ tenant_id=tenant_id,
+ email=email)
+ user_id = user['id']
+ self.addCleanup(identity_client.delete_user, user_id)
+
+ self.adm_client.update_quota_set(tenant_id,
+ user_id=user_id,
+ ram='2048')
+ resp, quota_set = self.adm_client.get_quota_set(tenant_id,
+ user_id=user_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(2048, quota_set['ram'])
@test.attr(type='gate')
def test_delete_quota(self):
diff --git a/tempest/api/compute/v3/servers/test_instance_actions.py b/tempest/api/compute/v3/servers/test_instance_actions.py
index 7d25100..399541b 100644
--- a/tempest/api/compute/v3/servers/test_instance_actions.py
+++ b/tempest/api/compute/v3/servers/test_instance_actions.py
@@ -27,25 +27,27 @@
cls.resp = resp
cls.server_id = server['id']
+ @test.skip_because(bug="1206032")
@test.attr(type='gate')
- def test_list_instance_actions(self):
+ def test_list_server_actions(self):
# List actions of the provided server
resp, body = self.client.reboot(self.server_id, 'HARD')
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
- resp, body = self.client.list_instance_actions(self.server_id)
+ resp, body = self.client.list_server_actions(self.server_id)
self.assertEqual(200, resp.status)
self.assertTrue(len(body) == 2, str(body))
self.assertTrue(any([i for i in body if i['action'] == 'create']))
self.assertTrue(any([i for i in body if i['action'] == 'reboot']))
+ @test.skip_because(bug="1206032")
@test.attr(type='gate')
@test.skip_because(bug="1281915")
- def test_get_instance_action(self):
+ def test_get_server_action(self):
# Get the action details of the provided server
request_id = self.resp['x-compute-request-id']
- resp, body = self.client.get_instance_action(self.server_id,
- request_id)
+ resp, body = self.client.get_server_action(self.server_id,
+ request_id)
self.assertEqual(200, resp.status)
- self.assertEqual(self.server_id, body['instance_uuid'])
+ self.assertEqual(self.server_id, body['server_uuid'])
self.assertEqual('create', body['action'])
diff --git a/tempest/api/compute/v3/servers/test_instance_actions_negative.py b/tempest/api/compute/v3/servers/test_instance_actions_negative.py
index b0a7050..0b2c6f9 100644
--- a/tempest/api/compute/v3/servers/test_instance_actions_negative.py
+++ b/tempest/api/compute/v3/servers/test_instance_actions_negative.py
@@ -29,15 +29,15 @@
cls.server_id = server['id']
@test.attr(type=['negative', 'gate'])
- def test_list_instance_actions_invalid_server(self):
+ def test_list_server_actions_invalid_server(self):
# List actions of the invalid server id
invalid_server_id = data_utils.rand_uuid()
self.assertRaises(exceptions.NotFound,
- self.client.list_instance_actions, invalid_server_id)
+ self.client.list_server_actions, invalid_server_id)
@test.attr(type=['negative', 'gate'])
- def test_get_instance_action_invalid_request(self):
+ def test_get_server_action_invalid_request(self):
# Get the action details of the provided server with invalid request
invalid_request_id = 'req-' + data_utils.rand_uuid()
- self.assertRaises(exceptions.NotFound, self.client.get_instance_action,
+ self.assertRaises(exceptions.NotFound, self.client.get_server_action,
self.server_id, invalid_request_id)
diff --git a/tempest/api/identity/admin/v3/test_roles.py b/tempest/api/identity/admin/v3/test_roles.py
index 24c7b83..90dccca 100644
--- a/tempest/api/identity/admin/v3/test_roles.py
+++ b/tempest/api/identity/admin/v3/test_roles.py
@@ -73,7 +73,7 @@
self.assertIn(role_id, fetched_role_ids)
@test.attr(type='smoke')
- def test_role_create_update_get(self):
+ def test_role_create_update_get_list(self):
r_name = data_utils.rand_name('Role-')
resp, role = self.client.create_role(r_name)
self.addCleanup(self.client.delete_role, role['id'])
@@ -94,6 +94,10 @@
self.assertEqual(new_name, new_role['name'])
self.assertEqual(updated_role['id'], new_role['id'])
+ resp, roles = self.client.list_roles()
+ self.assertEqual(resp['status'], '200')
+ self.assertIn(role['id'], [r['id'] for r in roles])
+
@test.attr(type='smoke')
def test_grant_list_revoke_role_to_user_on_project(self):
resp, _ = self.client.assign_user_role_on_project(
diff --git a/tempest/api/network/admin/test_agent_management.py b/tempest/api/network/admin/test_agent_management.py
index 342bc6a..b848994 100644
--- a/tempest/api/network/admin/test_agent_management.py
+++ b/tempest/api/network/admin/test_agent_management.py
@@ -37,8 +37,10 @@
agents = body['agents']
# Hearthbeats must be excluded from comparison
self.agent.pop('heartbeat_timestamp', None)
+ self.agent.pop('configurations', None)
for agent in agents:
agent.pop('heartbeat_timestamp', None)
+ agent.pop('configurations', None)
self.assertIn(self.agent, agents)
@test.attr(type=['smoke'])
diff --git a/tempest/api/network/admin/test_dhcp_agent_scheduler.py b/tempest/api/network/admin/test_dhcp_agent_scheduler.py
index 13ae1c0..25e1cc0 100644
--- a/tempest/api/network/admin/test_dhcp_agent_scheduler.py
+++ b/tempest/api/network/admin/test_dhcp_agent_scheduler.py
@@ -20,6 +20,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(DHCPAgentSchedulersTestJSON, cls).setUpClass()
if not test.is_extension_enabled('dhcp_agent_scheduler', 'network'):
diff --git a/tempest/api/network/admin/test_lbaas_agent_scheduler.py b/tempest/api/network/admin/test_lbaas_agent_scheduler.py
index a5ba90f..675c62d 100644
--- a/tempest/api/network/admin/test_lbaas_agent_scheduler.py
+++ b/tempest/api/network/admin/test_lbaas_agent_scheduler.py
@@ -35,6 +35,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(LBaaSAgentSchedulerTestJSON, cls).setUpClass()
if not test.is_extension_enabled('lbaas_agent_scheduler', 'network'):
diff --git a/tempest/api/network/admin/test_load_balancer_admin_actions.py b/tempest/api/network/admin/test_load_balancer_admin_actions.py
index 34a8e32..6bcc118 100644
--- a/tempest/api/network/admin/test_load_balancer_admin_actions.py
+++ b/tempest/api/network/admin/test_load_balancer_admin_actions.py
@@ -29,6 +29,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(LoadBalancerAdminTestJSON, cls).setUpClass()
if not test.is_extension_enabled('lbaas', 'network'):
@@ -89,6 +90,18 @@
show_health_monitor = body['health_monitor']
self.assertEqual(health_monitor['id'], show_health_monitor['id'])
+ @test.attr(type='smoke')
+ def test_create_pool_from_admin_user_other_tenant(self):
+ resp, body = self.admin_client.create_pool(
+ name=data_utils.rand_name('pool-'), lb_method="ROUND_ROBIN",
+ protocol="HTTP", subnet_id=self.subnet['id'],
+ tenant_id=self.tenant_id)
+ self.assertEqual('201', resp['status'])
+ pool = body['pool']
+ self.addCleanup(self.admin_client.delete_pool, pool['id'])
+ self.assertIsNotNone(pool['id'])
+ self.assertEqual(self.tenant_id, pool['tenant_id'])
+
class LoadBalancerAdminTestXML(LoadBalancerAdminTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/test_extra_dhcp_options.py b/tempest/api/network/test_extra_dhcp_options.py
index ed86d75..371c651 100644
--- a/tempest/api/network/test_extra_dhcp_options.py
+++ b/tempest/api/network/test_extra_dhcp_options.py
@@ -36,6 +36,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(ExtraDHCPOptionsTestJSON, cls).setUpClass()
if not test.is_extension_enabled('extra_dhcp_opt', 'network'):
diff --git a/tempest/api/network/test_floating_ips.py b/tempest/api/network/test_floating_ips.py
index 06871ad..7191940 100644
--- a/tempest/api/network/test_floating_ips.py
+++ b/tempest/api/network/test_floating_ips.py
@@ -44,6 +44,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(FloatingIPTestJSON, cls).setUpClass()
if not test.is_extension_enabled('router', 'network'):
diff --git a/tempest/api/network/test_load_balancer.py b/tempest/api/network/test_load_balancer.py
index 792d61d..3ab015e 100644
--- a/tempest/api/network/test_load_balancer.py
+++ b/tempest/api/network/test_load_balancer.py
@@ -38,6 +38,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(LoadBalancerTestJSON, cls).setUpClass()
if not test.is_extension_enabled('lbaas', 'network'):
@@ -156,10 +157,14 @@
# Verification of pool update
new_name = "New_pool"
resp, body = self.client.update_pool(pool['id'],
- name=new_name)
+ name=new_name,
+ description="new_description",
+ lb_method='LEAST_CONNECTIONS')
self.assertEqual('200', resp['status'])
updated_pool = body['pool']
- self.assertEqual(updated_pool['name'], new_name)
+ self.assertEqual(new_name, updated_pool['name'])
+ self.assertEqual('new_description', updated_pool['description'])
+ self.assertEqual('LEAST_CONNECTIONS', updated_pool['lb_method'])
# Verification of pool delete
resp, body = self.client.delete_pool(pool['id'])
self.assertEqual('204', resp['status'])
@@ -377,6 +382,58 @@
self.assertIn("active_connections", stats)
self.assertIn("bytes_out", stats)
+ @test.attr(type='smoke')
+ def test_update_list_of_health_monitors_associated_with_pool(self):
+ resp, _ = (self.client.associate_health_monitor_with_pool
+ (self.health_monitor['id'], self.pool['id']))
+ self.assertEqual('201', resp['status'])
+ resp, _ = self.client.update_health_monitor(
+ self.health_monitor['id'], admin_state_up=False)
+ self.assertEqual('200', resp['status'])
+ resp, body = self.client.show_pool(self.pool['id'])
+ self.assertEqual('200', resp['status'])
+ health_monitors = body['pool']['health_monitors']
+ for health_monitor_id in health_monitors:
+ resp, body = self.client.show_health_monitor(health_monitor_id)
+ self.assertEqual('200', resp['status'])
+ self.assertFalse(body['health_monitor']['admin_state_up'])
+ resp, _ = (self.client.disassociate_health_monitor_with_pool
+ (self.health_monitor['id'], self.pool['id']))
+ self.assertEqual('204', resp['status'])
+
+ @test.attr(type='smoke')
+ def test_update_admin_state_up_of_pool(self):
+ resp, _ = self.client.update_pool(self.pool['id'],
+ admin_state_up=False)
+ self.assertEqual('200', resp['status'])
+ resp, body = self.client.show_pool(self.pool['id'])
+ self.assertEqual('200', resp['status'])
+ pool = body['pool']
+ self.assertFalse(pool['admin_state_up'])
+
+ @test.attr(type='smoke')
+ def test_show_vip_associated_with_pool(self):
+ resp, body = self.client.show_pool(self.pool['id'])
+ self.assertEqual('200', resp['status'])
+ pool = body['pool']
+ resp, body = self.client.show_vip(pool['vip_id'])
+ self.assertEqual('200', resp['status'])
+ vip = body['vip']
+ self.assertEqual(self.vip['name'], vip['name'])
+ self.assertEqual(self.vip['id'], vip['id'])
+
+ @test.attr(type='smoke')
+ def test_show_members_associated_with_pool(self):
+ resp, body = self.client.show_pool(self.pool['id'])
+ self.assertEqual('200', resp['status'])
+ members = body['pool']['members']
+ for member_id in members:
+ resp, body = self.client.show_member(member_id)
+ self.assertEqual('200', resp['status'])
+ self.assertIsNotNone(body['member']['status'])
+ self.assertEqual(member_id, body['member']['id'])
+ self.assertIsNotNone(body['member']['admin_state_up'])
+
class LoadBalancerTestXML(LoadBalancerTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/test_networks.py b/tempest/api/network/test_networks.py
index b9041ee..0175de7 100644
--- a/tempest/api/network/test_networks.py
+++ b/tempest/api/network/test_networks.py
@@ -19,7 +19,7 @@
from tempest.common.utils import data_utils
from tempest import config
from tempest import exceptions
-from tempest.test import attr
+from tempest import test
CONF = config.CONF
@@ -64,6 +64,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(NetworksTestJSON, cls).setUpClass()
cls.network = cls.create_network()
@@ -71,7 +72,7 @@
cls.subnet = cls.create_subnet(cls.network)
cls.cidr = cls.subnet['cidr']
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_create_update_delete_network_subnet(self):
# Create a network
name = data_utils.rand_name('network-')
@@ -102,7 +103,7 @@
resp, body = self.client.delete_network(net_id)
self.assertEqual('204', resp['status'])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_show_network(self):
# Verify the details of a network
resp, body = self.client.show_network(self.network['id'])
@@ -111,7 +112,7 @@
for key in ['id', 'name']:
self.assertEqual(network[key], self.network[key])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_show_network_fields(self):
# Verify specific fields of a network
field_list = [('fields', 'id'), ('fields', 'name'), ]
@@ -123,7 +124,7 @@
for label, field_name in field_list:
self.assertEqual(network[field_name], self.network[field_name])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_list_networks(self):
# Verify the network exists in the list of all networks
resp, body = self.client.list_networks()
@@ -132,7 +133,7 @@
if network['id'] == self.network['id']]
self.assertNotEmpty(networks, "Created network not found in the list")
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_list_networks_fields(self):
# Verify specific fields of the networks
resp, body = self.client.list_networks(fields='id')
@@ -143,7 +144,7 @@
self.assertEqual(len(network), 1)
self.assertIn('id', network)
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_show_subnet(self):
# Verify the details of a subnet
resp, body = self.client.show_subnet(self.subnet['id'])
@@ -154,7 +155,7 @@
self.assertIn(key, subnet)
self.assertEqual(subnet[key], self.subnet[key])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_show_subnet_fields(self):
# Verify specific fields of a subnet
field_list = [('fields', 'id'), ('fields', 'cidr'), ]
@@ -166,7 +167,7 @@
for label, field_name in field_list:
self.assertEqual(subnet[field_name], self.subnet[field_name])
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_list_subnets(self):
# Verify the subnet exists in the list of all subnets
resp, body = self.client.list_subnets()
@@ -175,7 +176,7 @@
if subnet['id'] == self.subnet['id']]
self.assertNotEmpty(subnets, "Created subnet not found in the list")
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_list_subnets_fields(self):
# Verify specific fields of subnets
resp, body = self.client.list_subnets(fields='id')
@@ -194,7 +195,7 @@
except exceptions.NotFound:
pass
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_delete_network_with_subnet(self):
# Creates a network
name = data_utils.rand_name('network-')
@@ -221,7 +222,7 @@
# it from the list.
self.subnets.pop()
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_create_port_with_no_ip(self):
# For this test create a new network - do not use any previously
# created networks.
@@ -275,6 +276,7 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(BulkNetworkOpsTestJSON, cls).setUpClass()
cls.network1 = cls.create_network()
@@ -310,7 +312,7 @@
for n in created_ports:
self.assertNotIn(n['id'], ports_list)
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_bulk_create_delete_network(self):
# Creates 2 networks in one request
network_names = [data_utils.rand_name('network-'),
@@ -326,7 +328,7 @@
self.assertIsNotNone(n['id'])
self.assertIn(n['id'], networks_list)
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_bulk_create_delete_subnet(self):
# Creates 2 subnets in one request
cidr = netaddr.IPNetwork(CONF.network.tenant_network_cidr)
@@ -358,7 +360,7 @@
self.assertIsNotNone(n['id'])
self.assertIn(n['id'], subnets_list)
- @attr(type='smoke')
+ @test.attr(type='smoke')
def test_bulk_create_delete_port(self):
# Creates 2 ports in one request
networks = [self.network1['id'], self.network2['id']]
diff --git a/tempest/api/network/test_ports.py b/tempest/api/network/test_ports.py
index fbb25a8..66dcaa5 100644
--- a/tempest/api/network/test_ports.py
+++ b/tempest/api/network/test_ports.py
@@ -27,6 +27,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(PortsTestJSON, cls).setUpClass()
cls.network = cls.create_network()
@@ -143,6 +144,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(PortsAdminExtendedAttrsTestJSON, cls).setUpClass()
cls.identity_client = cls._get_identity_admin_client()
diff --git a/tempest/api/network/test_routers_negative.py b/tempest/api/network/test_routers_negative.py
index e6ad4de..91ab9d6 100644
--- a/tempest/api/network/test_routers_negative.py
+++ b/tempest/api/network/test_routers_negative.py
@@ -23,6 +23,7 @@
_interface = 'json'
@classmethod
+ @test.safe_setup
def setUpClass(cls):
super(RoutersNegativeTest, cls).setUpClass()
if not test.is_extension_enabled('router', 'network'):
diff --git a/tempest/api/network/test_vpnaas_extensions.py b/tempest/api/network/test_vpnaas_extensions.py
index 78bc80a..7edaaf8 100644
--- a/tempest/api/network/test_vpnaas_extensions.py
+++ b/tempest/api/network/test_vpnaas_extensions.py
@@ -21,7 +21,7 @@
CONF = config.CONF
-class VPNaaSJSON(base.BaseNetworkTest):
+class VPNaaSTestJSON(base.BaseNetworkTest):
_interface = 'json'
"""
@@ -37,11 +37,12 @@
"""
@classmethod
+ @test.safe_setup
def setUpClass(cls):
if not test.is_extension_enabled('vpnaas', 'network'):
msg = "vpnaas extension not enabled."
raise cls.skipException(msg)
- super(VPNaaSJSON, cls).setUpClass()
+ super(VPNaaSTestJSON, cls).setUpClass()
cls.network = cls.create_network()
cls.subnet = cls.create_subnet(cls.network)
cls.router = cls.create_router(
@@ -175,3 +176,7 @@
ikepolicy['phase1_negotiation_mode'])
self.assertEqual(self.ikepolicy['ike_version'],
ikepolicy['ike_version'])
+
+
+class VPNaaSTestXML(VPNaaSTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/object_storage/test_account_quotas.py b/tempest/api/object_storage/test_account_quotas.py
index a3098a5..d919245 100644
--- a/tempest/api/object_storage/test_account_quotas.py
+++ b/tempest/api/object_storage/test_account_quotas.py
@@ -82,7 +82,8 @@
# Set a quota of 20 bytes on the user's account before each test
headers = {"X-Account-Meta-Quota-Bytes": "20"}
- self.os.custom_account_client.request("POST", "", headers, "")
+ self.os.custom_account_client.request("POST", url="", headers=headers,
+ body="")
def tearDown(self):
# Set the reselleradmin auth in headers for next custom_account_client
@@ -94,7 +95,8 @@
# remove the quota from the container
headers = {"X-Remove-Account-Meta-Quota-Bytes": "x"}
- self.os.custom_account_client.request("POST", "", headers, "")
+ self.os.custom_account_client.request("POST", url="", headers=headers,
+ body="")
super(AccountQuotasTest, self).tearDown()
@classmethod
@@ -135,8 +137,9 @@
)
headers = {"X-Account-Meta-Quota-Bytes": quota}
- resp, _ = self.os.custom_account_client.request("POST", "",
- headers, "")
+ resp, _ = self.os.custom_account_client.request("POST", url="",
+ headers=headers,
+ body="")
self.assertEqual(resp["status"], "204")
self.assertHeaders(resp, 'Account', 'POST')
diff --git a/tempest/api/object_storage/test_account_quotas_negative.py b/tempest/api/object_storage/test_account_quotas_negative.py
index 7648ea1..29afebc 100644
--- a/tempest/api/object_storage/test_account_quotas_negative.py
+++ b/tempest/api/object_storage/test_account_quotas_negative.py
@@ -81,7 +81,8 @@
# Set a quota of 20 bytes on the user's account before each test
headers = {"X-Account-Meta-Quota-Bytes": "20"}
- self.os.custom_account_client.request("POST", "", headers, "")
+ self.os.custom_account_client.request("POST", url="", headers=headers,
+ body="")
def tearDown(self):
# Set the reselleradmin auth in headers for next custom_account_client
@@ -93,7 +94,8 @@
# remove the quota from the container
headers = {"X-Remove-Account-Meta-Quota-Bytes": "x"}
- self.os.custom_account_client.request("POST", "", headers, "")
+ self.os.custom_account_client.request("POST", url="", headers=headers,
+ body="")
super(AccountQuotasNegativeTest, self).tearDown()
@classmethod
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index 91df292..b057698 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -14,6 +14,7 @@
# under the License.
import hashlib
+import re
from six import moves
from tempest.api.object_storage import base
@@ -35,6 +36,29 @@
cls.delete_containers(cls.containers)
super(ObjectTest, cls).tearDownClass()
+ def _create_object(self, metadata=None):
+ # setup object
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ self.object_client.create_object(self.container_name,
+ object_name, data, metadata=metadata)
+
+ return object_name, data
+
+ def _upload_segments(self):
+ # create object
+ object_name = data_utils.rand_name(name='LObject')
+ data = data_utils.arbitrary_string()
+ segments = 10
+ data_segments = [data + str(i) for i in moves.xrange(segments)]
+ # uploading segments
+ for i in moves.xrange(segments):
+ resp, _ = self.object_client.create_object_segments(
+ self.container_name, object_name, i, data_segments[i])
+ self.assertEqual(resp['status'], '201')
+
+ return object_name, data_segments
+
@test.attr(type='smoke')
def test_create_object(self):
# create object
@@ -64,32 +88,220 @@
self.assertHeaders(resp, 'Object', 'DELETE')
@test.attr(type='smoke')
- def test_object_metadata(self):
- # add metadata to storage object, test if metadata is retrievable
+ def test_update_object_metadata(self):
+ # update object metadata
+ object_name, data = self._create_object()
- # create Object
- object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- resp, _ = self.object_client.create_object(self.container_name,
- object_name, data)
- # set object metadata
- meta_key = data_utils.rand_name(name='test-')
- meta_value = data_utils.rand_name(name='MetaValue-')
- orig_metadata = {meta_key: meta_value}
+ metadata = {'X-Object-Meta-test-meta': 'Meta'}
resp, _ = self.object_client.update_object_metadata(
- self.container_name, object_name, orig_metadata)
+ self.container_name,
+ object_name,
+ metadata,
+ metadata_prefix='')
self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
self.assertHeaders(resp, 'Object', 'POST')
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn('x-object-meta-test-meta', resp)
+ self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
+
+ def test_update_object_metadata_with_remove_metadata(self):
+ # update object metadata with remove metadata
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ create_metadata = {'X-Object-Meta-test-meta1': 'Meta1'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ update_metadata = {'X-Remove-Object-Meta-test-meta1': 'Meta1'}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertNotIn('x-object-meta-test-meta1', resp)
+
+ @test.attr(type='smoke')
+ def test_update_object_metadata_with_create_and_remove_metadata(self):
+ # creation and deletion of metadata with one request
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ create_metadata = {'X-Object-Meta-test-meta1': 'Meta1'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ update_metadata = {'X-Object-Meta-test-meta2': 'Meta2',
+ 'X-Remove-Object-Meta-test-meta1': 'Meta1'}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertNotIn('x-object-meta-test-meta1', resp)
+ self.assertIn('x-object-meta-test-meta2', resp)
+ self.assertEqual(resp['x-object-meta-test-meta2'], 'Meta2')
+
+ @test.attr(type='smoke')
+ def test_update_object_metadata_with_x_object_manifest(self):
+ # update object metadata with x_object_manifest
+
+ # uploading segments
+ object_name, data_segments = self._upload_segments()
+ # creating a manifest file
+ data_empty = ''
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data_empty,
+ metadata=None)
+ object_prefix = '%s/%s' % (self.container_name, object_name)
+ update_metadata = {'X-Object-Manifest': object_prefix}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn('x-object-manifest', resp)
+ self.assertNotEqual(len(resp['x-object-manifest']), 0)
+
+ def test_update_object_metadata_with_x_object_metakey(self):
+ # update object metadata with a blenk value of metadata
+ object_name, data = self._create_object()
+
+ update_metadata = {'X-Object-Meta-test-meta': ''}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn('x-object-meta-test-meta', resp)
+ self.assertEqual(resp['x-object-meta-test-meta'], '')
+
+ @test.attr(type='smoke')
+ def test_update_object_metadata_with_x_remove_object_metakey(self):
+ # update object metadata with a blank value of remove metadata
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ create_metadata = {'X-Object-Meta-test-meta': 'Meta'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ update_metadata = {'X-Remove-Object-Meta-test-meta': ''}
+ resp, _ = self.object_client.update_object_metadata(
+ self.container_name,
+ object_name,
+ update_metadata,
+ metadata_prefix='')
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'POST')
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertNotIn('x-object-meta-test-meta', resp)
+
+ @test.attr(type='smoke')
+ def test_list_object_metadata(self):
# get object metadata
- resp, resp_metadata = self.object_client.list_object_metadata(
- self.container_name, object_name)
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ metadata = {'X-Object-Meta-test-meta': 'Meta'}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=metadata)
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
self.assertHeaders(resp, 'Object', 'HEAD')
+ self.assertIn('x-object-meta-test-meta', resp)
+ self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
- actual_meta_key = 'x-object-meta-' + meta_key
- self.assertIn(actual_meta_key, resp)
- self.assertEqual(resp[actual_meta_key], meta_value)
+ @test.attr(type='smoke')
+ def test_list_no_object_metadata(self):
+ # get empty list of object metadata
+ object_name, data = self._create_object()
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'HEAD')
+ self.assertNotIn('x-object-meta-', str(resp))
+
+ @test.attr(type='smoke')
+ def test_list_object_metadata_with_x_object_manifest(self):
+ # get object metadata with x_object_manifest
+
+ # uploading segments
+ object_name, data_segments = self._upload_segments()
+ # creating a manifest file
+ object_prefix = '%s/%s' % (self.container_name, object_name)
+ metadata = {'X-Object-Manifest': object_prefix}
+ data_empty = ''
+ resp, _ = self.object_client.create_object(
+ self.container_name,
+ object_name,
+ data_empty,
+ metadata=metadata)
+
+ resp, _ = self.object_client.list_object_metadata(
+ self.container_name,
+ object_name)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+
+ # Check only the existence of common headers with custom matcher
+ self.assertThat(resp, custom_matchers.ExistsAllResponseHeaders(
+ 'Object', 'HEAD'))
+ self.assertIn('x-object-manifest', resp)
+
+ # Etag value of a large object is enclosed in double-quotations.
+ # This is a special case, therefore the formats of response headers
+ # are checked without a custom matcher.
+ self.assertTrue(resp['etag'].startswith('\"'))
+ self.assertTrue(resp['etag'].endswith('\"'))
+ self.assertTrue(resp['etag'].strip('\"').isalnum())
+ self.assertTrue(re.match("^\d+\.?\d*\Z", resp['x-timestamp']))
+ self.assertNotEqual(len(resp['content-type']), 0)
+ self.assertTrue(re.match("^tx[0-9a-f]*-[0-9a-f]*$",
+ resp['x-trans-id']))
+ self.assertNotEqual(len(resp['date']), 0)
+ self.assertEqual(resp['accept-ranges'], 'bytes')
+ self.assertEqual(resp['x-object-manifest'],
+ '%s/%s' % (self.container_name, object_name))
@test.attr(type='smoke')
def test_get_object(self):
diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py
index 1832259..7656ff3 100644
--- a/tempest/api/orchestration/base.py
+++ b/tempest/api/orchestration/base.py
@@ -120,3 +120,20 @@
"""Return a stack output value for a given key."""
return next((o['output_value'] for o in stack['outputs']
if o['output_key'] == output_key), None)
+
+ def assert_fields_in_dict(self, obj, *fields):
+ for field in fields:
+ self.assertIn(field, obj)
+
+ def list_resources(self, stack_identifier):
+ """Get a dict mapping of resource names to types."""
+ resp, resources = self.client.list_resources(stack_identifier)
+ self.assertEqual('200', resp['status'])
+ self.assertIsInstance(resources, list)
+ for res in resources:
+ self.assert_fields_in_dict(res, 'logical_resource_id',
+ 'resource_type', 'resource_status',
+ 'updated_time')
+
+ return dict((r['resource_name'], r['resource_type'])
+ for r in resources)
diff --git a/tempest/api/orchestration/stacks/test_non_empty_stack.py b/tempest/api/orchestration/stacks/test_non_empty_stack.py
index 4b1b5ef..9ef95a1 100644
--- a/tempest/api/orchestration/stacks/test_non_empty_stack.py
+++ b/tempest/api/orchestration/stacks/test_non_empty_stack.py
@@ -40,16 +40,18 @@
cls.resource_type = 'AWS::AutoScaling::LaunchConfiguration'
cls.client.wait_for_stack_status(cls.stack_id, 'CREATE_COMPLETE')
- def assert_fields_in_dict(self, obj, *fields):
- for field in fields:
- self.assertIn(field, obj)
+ def _list_stacks(self, expected_num=None, **filter_kwargs):
+ resp, stacks = self.client.list_stacks(params=filter_kwargs)
+ self.assertEqual('200', resp['status'])
+ self.assertIsInstance(stacks, list)
+ if expected_num is not None:
+ self.assertEqual(expected_num, len(stacks))
+ return stacks
@attr(type='gate')
def test_stack_list(self):
"""Created stack should be in the list of existing stacks."""
- resp, stacks = self.client.list_stacks()
- self.assertEqual('200', resp['status'])
- self.assertIsInstance(stacks, list)
+ stacks = self._list_stacks()
stacks_names = map(lambda stack: stack['stack_name'], stacks)
self.assertIn(self.stack_name, stacks_names)
@@ -89,20 +91,8 @@
def test_list_resources(self):
"""Getting list of created resources for the stack should be possible.
"""
- resp, resources = self.client.list_resources(self.stack_identifier)
- self.assertEqual('200', resp['status'])
- self.assertIsInstance(resources, list)
- for res in resources:
- self.assert_fields_in_dict(res, 'logical_resource_id',
- 'resource_type', 'resource_status',
- 'updated_time')
-
- resources_names = map(lambda resource: resource['logical_resource_id'],
- resources)
- self.assertIn(self.resource_name, resources_names)
- resources_types = map(lambda resource: resource['resource_type'],
- resources)
- self.assertIn(self.resource_type, resources_types)
+ resources = self.list_resources(self.stack_identifier)
+ self.assertEqual({self.resource_name: self.resource_type}, resources)
@attr(type='gate')
def test_show_resource(self):
diff --git a/tempest/api/orchestration/stacks/test_server_cfn_init.py b/tempest/api/orchestration/stacks/test_server_cfn_init.py
index 5f65193..3f29269 100644
--- a/tempest/api/orchestration/stacks/test_server_cfn_init.py
+++ b/tempest/api/orchestration/stacks/test_server_cfn_init.py
@@ -93,7 +93,8 @@
try:
self.client.wait_for_resource_status(
sid, 'WaitCondition', 'CREATE_COMPLETE')
- except exceptions.TimeoutException as e:
+ except (exceptions.StackResourceBuildErrorException,
+ exceptions.TimeoutException) as e:
# attempt to log the server console to help with debugging
# the cause of the server not signalling the waitcondition
# to heat.
diff --git a/tempest/api/orchestration/stacks/test_update.py b/tempest/api/orchestration/stacks/test_update.py
new file mode 100644
index 0000000..a9a43b6
--- /dev/null
+++ b/tempest/api/orchestration/stacks/test_update.py
@@ -0,0 +1,84 @@
+# 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 logging
+
+from tempest.api.orchestration import base
+from tempest.common.utils import data_utils
+from tempest import test
+
+
+LOG = logging.getLogger(__name__)
+
+
+class UpdateStackTestJSON(base.BaseOrchestrationTest):
+ _interface = 'json'
+
+ template = '''
+heat_template_version: 2013-05-23
+resources:
+ random1:
+ type: OS::Heat::RandomString
+'''
+ update_template = '''
+heat_template_version: 2013-05-23
+resources:
+ random1:
+ type: OS::Heat::RandomString
+ random2:
+ type: OS::Heat::RandomString
+'''
+
+ def update_stack(self, stack_identifier, template):
+ stack_name = stack_identifier.split('/')[0]
+ resp = self.client.update_stack(
+ stack_identifier=stack_identifier,
+ name=stack_name,
+ template=template)
+ self.assertEqual('202', resp[0]['status'])
+ self.client.wait_for_stack_status(stack_identifier, 'UPDATE_COMPLETE')
+
+ @test.attr(type='gate')
+ def test_stack_update_nochange(self):
+ stack_name = data_utils.rand_name('heat')
+ stack_identifier = self.create_stack(stack_name, self.template)
+ self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+ expected_resources = {'random1': 'OS::Heat::RandomString'}
+ self.assertEqual(expected_resources,
+ self.list_resources(stack_identifier))
+
+ # Update with no changes, resources should be unchanged
+ self.update_stack(stack_identifier, self.template)
+ self.assertEqual(expected_resources,
+ self.list_resources(stack_identifier))
+
+ @test.attr(type='gate')
+ @test.skip_because(bug='1308682')
+ def test_stack_update_add_remove(self):
+ stack_name = data_utils.rand_name('heat')
+ stack_identifier = self.create_stack(stack_name, self.template)
+ self.client.wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+ initial_resources = {'random1': 'OS::Heat::RandomString'}
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
+
+ # Add one resource via a stack update
+ self.update_stack(stack_identifier, self.update_template)
+ updated_resources = {'random1': 'OS::Heat::RandomString',
+ 'random2': 'OS::Heat::RandomString'}
+ self.assertEqual(updated_resources,
+ self.list_resources(stack_identifier))
+
+ # Then remove it by updating with the original template
+ self.update_stack(stack_identifier, self.template)
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
diff --git a/tempest/api/volume/test_volumes_get.py b/tempest/api/volume/test_volumes_get.py
index be5d76b..58da440 100644
--- a/tempest/api/volume/test_volumes_get.py
+++ b/tempest/api/volume/test_volumes_get.py
@@ -51,8 +51,7 @@
v_name = data_utils.rand_name('Volume')
metadata = {'Type': 'Test'}
# Create a volume
- resp, volume = self.client.create_volume(size=1,
- display_name=v_name,
+ resp, volume = self.client.create_volume(display_name=v_name,
metadata=metadata,
**kwargs)
self.assertEqual(200, resp.status)
diff --git a/tempest/api_schema/compute/aggregates.py b/tempest/api_schema/compute/aggregates.py
index 49793fe..a70b356 100644
--- a/tempest/api_schema/compute/aggregates.py
+++ b/tempest/api_schema/compute/aggregates.py
@@ -12,6 +12,24 @@
# License for the specific language governing permissions and limitations
# under the License.
+aggregate = {
+ 'type': 'object',
+ 'properties:': {
+ 'availability_zone': {'type': ['string', 'null']},
+ 'created_at': {'type': 'string'},
+ 'deleted': {'type': 'boolean'},
+ 'deleted_at': {'type': ['string', 'null']},
+ 'hosts': {'type': 'array'},
+ 'id': {'type': 'integer'},
+ 'metadata': {'type': 'object'},
+ 'name': {'type': 'string'},
+ 'updated_at': {'type': ['string', 'null']}
+ },
+ 'required': ['availability_zone', 'created_at', 'deleted',
+ 'deleted_at', 'hosts', 'id', 'metadata',
+ 'name', 'updated_at']
+}
+
list_aggregates = {
'status_code': [200],
'response_body': {
@@ -19,25 +37,22 @@
'properties': {
'aggregates': {
'type': 'array',
- 'items': {
- 'type': 'object',
- 'properties': {
- 'availability_zone': {'type': ['string', 'null']},
- 'created_at': {'type': 'string'},
- 'deleted': {'type': 'boolean'},
- 'deleted_at': {'type': ['string', 'null']},
- 'hosts': {'type': 'array'},
- 'id': {'type': 'integer'},
- 'metadata': {'type': 'object'},
- 'name': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']}
- },
- 'required': ['availability_zone', 'created_at', 'deleted',
- 'deleted_at', 'hosts', 'id', 'metadata',
- 'name', 'updated_at']
- }
+ 'items': aggregate
}
},
'required': ['aggregates']
}
}
+
+get_aggregate = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'aggregate': aggregate
+ },
+ 'required': ['aggregate']
+ }
+}
+
+aggregate_set_metadata = get_aggregate
diff --git a/tempest/api_schema/compute/flavors.py b/tempest/api_schema/compute/flavors.py
index a6367d4..fd02780 100644
--- a/tempest/api_schema/compute/flavors.py
+++ b/tempest/api_schema/compute/flavors.py
@@ -35,3 +35,30 @@
'required': ['flavors']
}
}
+
+common_flavor_list_details = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'flavors': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'name': {'type': 'string'},
+ 'links': parameter_types.links,
+ 'ram': {'type': 'integer'},
+ 'vcpus': {'type': 'integer'},
+ 'swap': {'type': 'integer'},
+ 'disk': {'type': 'integer'},
+ 'id': {'type': 'string'}
+ },
+ 'required': ['name', 'links', 'ram', 'vcpus',
+ 'swap', 'disk', 'id']
+ }
+ }
+ },
+ 'required': ['flavors']
+ }
+}
diff --git a/tempest/api_schema/compute/flavors_access.py b/tempest/api_schema/compute/flavors_access.py
index 152e24c..cd31b0a 100644
--- a/tempest/api_schema/compute/flavors_access.py
+++ b/tempest/api_schema/compute/flavors_access.py
@@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-list_flavor_access = {
+add_remove_list_flavor_access = {
'status_code': [200],
'response_body': {
'type': 'object',
diff --git a/tempest/api_schema/compute/parameter_types.py b/tempest/api_schema/compute/parameter_types.py
index 67c0c9b..95d5b92 100644
--- a/tempest/api_schema/compute/parameter_types.py
+++ b/tempest/api_schema/compute/parameter_types.py
@@ -26,3 +26,8 @@
'required': ['href', 'rel']
}
}
+
+mac_address = {
+ 'type': 'string',
+ 'pattern': '(?:[a-f0-9]{2}:){5}[a-f0-9]{2}'
+}
diff --git a/tempest/api_schema/compute/servers.py b/tempest/api_schema/compute/servers.py
index 0071845..5eb5ecb 100644
--- a/tempest/api_schema/compute/servers.py
+++ b/tempest/api_schema/compute/servers.py
@@ -22,3 +22,24 @@
'required': ['password']
}
}
+
+get_vnc_console = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'console': {
+ 'type': 'object',
+ 'properties': {
+ 'type': {'type': 'string'},
+ 'url': {
+ 'type': 'string',
+ 'format': 'uri'
+ }
+ },
+ 'required': ['type', 'url']
+ }
+ },
+ 'required': ['console']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/agents.py b/tempest/api_schema/compute/v2/agents.py
new file mode 100644
index 0000000..837731f
--- /dev/null
+++ b/tempest/api_schema/compute/v2/agents.py
@@ -0,0 +1,17 @@
+# Copyright 2014 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.
+
+delete_agent = {
+ 'status_code': [200]
+}
diff --git a/tempest/api_schema/compute/v2/flavors.py b/tempest/api_schema/compute/v2/flavors.py
new file mode 100644
index 0000000..999ca19
--- /dev/null
+++ b/tempest/api_schema/compute/v2/flavors.py
@@ -0,0 +1,33 @@
+# Copyright 2014 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 copy
+
+from tempest.api_schema.compute import flavors
+
+list_flavors_details = copy.deepcopy(flavors.common_flavor_list_details)
+
+# 'swap' attributes comes as integre value but if it is empty it comes as "".
+# So defining type of as string and integer.
+list_flavors_details['response_body']['properties']['flavors']['items'][
+ 'properties']['swap'] = {'type': ['string', 'integer']}
+
+# Defining extra attributes for V2 flavor schema
+list_flavors_details['response_body']['properties']['flavors']['items'][
+ 'properties'].update({'OS-FLV-DISABLED:disabled': {'type': 'boolean'},
+ 'os-flavor-access:is_public': {'type': 'boolean'},
+ 'rxtx_factor': {'type': 'number'},
+ 'OS-FLV-EXT-DATA:ephemeral': {'type': 'integer'}})
+# 'OS-FLV-DISABLED', 'os-flavor-access', 'rxtx_factor' and 'OS-FLV-EXT-DATA'
+# are API extensions. So they are not 'required'.
diff --git a/tempest/api_schema/compute/v2/hosts.py b/tempest/api_schema/compute/v2/hosts.py
index add5bb5..cd6bd7b 100644
--- a/tempest/api_schema/compute/v2/hosts.py
+++ b/tempest/api_schema/compute/v2/hosts.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
+
body = {
'type': 'object',
'properties': {
@@ -25,3 +27,17 @@
'status_code': [200],
'response_body': body
}
+
+# The 'power_action' attribute of 'shutdown_host' API is 'shutdown'
+shutdown_host = copy.deepcopy(startup_host)
+
+shutdown_host['response_body']['properties']['power_action'] = {
+ 'enum': ['shutdown']
+}
+
+# The 'power_action' attribute of 'reboot_host' API is 'reboot'
+reboot_host = copy.deepcopy(startup_host)
+
+reboot_host['response_body']['properties']['power_action'] = {
+ 'enum': ['reboot']
+}
diff --git a/tempest/api_schema/compute/v2/instance_usage_audit_logs.py b/tempest/api_schema/compute/v2/instance_usage_audit_logs.py
index c1509b4..658f574 100644
--- a/tempest/api_schema/compute/v2/instance_usage_audit_logs.py
+++ b/tempest/api_schema/compute/v2/instance_usage_audit_logs.py
@@ -46,3 +46,14 @@
'required': ['instance_usage_audit_log']
}
}
+
+list_instance_usage_audit_log = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'instance_usage_audit_logs': common_instance_usage_audit_log
+ },
+ 'required': ['instance_usage_audit_logs']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/quotas.py b/tempest/api_schema/compute/v2/quotas.py
index d69cbd7..17dc4dd 100644
--- a/tempest/api_schema/compute/v2/quotas.py
+++ b/tempest/api_schema/compute/v2/quotas.py
@@ -45,3 +45,7 @@
'required': ['quota_set']
}
}
+
+delete_quota = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/v2/servers.py b/tempest/api_schema/compute/v2/servers.py
index 4e0cec0..81d2ef7 100644
--- a/tempest/api_schema/compute/v2/servers.py
+++ b/tempest/api_schema/compute/v2/servers.py
@@ -42,3 +42,27 @@
'required': ['server']
}
}
+
+list_virtual_interfaces = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'virtual_interfaces': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'mac_address': parameter_types.mac_address,
+ 'OS-EXT-VIF-NET:net_id': {'type': 'string'}
+ },
+ # 'OS-EXT-VIF-NET:net_id' is API extension So it is
+ # not defined as 'required'
+ 'required': ['id', 'mac_address']
+ }
+ }
+ },
+ 'required': ['virtual_interfaces']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/volumes.py b/tempest/api_schema/compute/v2/volumes.py
index 9cfd7e3..84a659c 100644
--- a/tempest/api_schema/compute/v2/volumes.py
+++ b/tempest/api_schema/compute/v2/volumes.py
@@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-get_volume = {
+create_get_volume = {
'status_code': [200],
'response_body': {
'type': 'object',
@@ -108,3 +108,7 @@
'required': ['volumes']
}
}
+
+delete_volume = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/v3/agents.py b/tempest/api_schema/compute/v3/agents.py
new file mode 100644
index 0000000..63d1c46
--- /dev/null
+++ b/tempest/api_schema/compute/v3/agents.py
@@ -0,0 +1,17 @@
+# Copyright 2014 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.
+
+delete_agent = {
+ 'status_code': [204]
+}
diff --git a/tempest/api_schema/compute/v3/flavors.py b/tempest/api_schema/compute/v3/flavors.py
new file mode 100644
index 0000000..542d2b1
--- /dev/null
+++ b/tempest/api_schema/compute/v3/flavors.py
@@ -0,0 +1,33 @@
+# Copyright 2014 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 copy
+
+from tempest.api_schema.compute import flavors
+
+list_flavors_details = copy.deepcopy(flavors.common_flavor_list_details)
+
+# NOTE- In v3 API, 'swap' comes as '0' not empty string '""'
+# (In V2 API, it comes as empty string) So leaving 'swap'as integer type only.
+
+# Defining extra attributes for V3 flavor schema
+list_flavors_details['response_body']['properties']['flavors']['items'][
+ 'properties'].update({'disabled': {'type': 'boolean'},
+ 'ephemeral': {'type': 'integer'},
+ 'flavor-access:is_public': {'type': 'boolean'},
+ 'os-flavor-rxtx:rxtx_factor': {'type': 'number'}})
+# 'flavor-access' and 'os-flavor-rxtx' are API extensions.
+# So they are not 'required'.
+list_flavors_details['response_body']['properties']['flavors']['items'][
+ 'required'].extend(['disabled', 'ephemeral'])
diff --git a/tempest/api_schema/compute/v3/hosts.py b/tempest/api_schema/compute/v3/hosts.py
index 9731d4b..2cf8f9b 100644
--- a/tempest/api_schema/compute/v3/hosts.py
+++ b/tempest/api_schema/compute/v3/hosts.py
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
from tempest.api_schema.compute.v2 import hosts
startup_host = {
@@ -24,3 +25,17 @@
'required': ['host']
}
}
+
+# The 'power_action' attribute of 'shutdown_host' API is 'shutdown'
+shutdown_host = copy.deepcopy(startup_host)
+
+shutdown_host['response_body']['properties']['power_action'] = {
+ 'enum': ['shutdown']
+}
+
+# The 'power_action' attribute of 'reboot_host' API is 'reboot'
+reboot_host = copy.deepcopy(startup_host)
+
+reboot_host['response_body']['properties']['power_action'] = {
+ 'enum': ['reboot']
+}
diff --git a/tempest/api_schema/compute/v3/quotas.py b/tempest/api_schema/compute/v3/quotas.py
index 5378c0f..aec1e80 100644
--- a/tempest/api_schema/compute/v3/quotas.py
+++ b/tempest/api_schema/compute/v3/quotas.py
@@ -79,3 +79,7 @@
'required': ['quota_set']
}
}
+
+delete_quota = {
+ 'status_code': [204]
+}
diff --git a/tempest/cli/simple_read_only/test_sahara.py b/tempest/cli/simple_read_only/test_sahara.py
index 5af8dae..36cc324 100644
--- a/tempest/cli/simple_read_only/test_sahara.py
+++ b/tempest/cli/simple_read_only/test_sahara.py
@@ -93,3 +93,48 @@
'status',
'node_count'
])
+
+ def test_sahara_data_source_list(self):
+ result = self.sahara('data-source-list')
+ data_sources = self.parser.listing(result)
+ self.assertTableStruct(data_sources, [
+ 'name',
+ 'id',
+ 'type',
+ 'description'
+ ])
+
+ def test_sahara_job_binary_data_list(self):
+ result = self.sahara('job-binary-data-list')
+ job_binary_data_list = self.parser.listing(result)
+ self.assertTableStruct(job_binary_data_list, [
+ 'id',
+ 'name'
+ ])
+
+ def test_sahara_job_binary_list(self):
+ result = self.sahara('job-binary-list')
+ job_binaries = self.parser.listing(result)
+ self.assertTableStruct(job_binaries, [
+ 'id',
+ 'name',
+ 'description'
+ ])
+
+ def test_sahara_job_template_list(self):
+ result = self.sahara('job-template-list')
+ job_templates = self.parser.listing(result)
+ self.assertTableStruct(job_templates, [
+ 'id',
+ 'name',
+ 'description'
+ ])
+
+ def test_sahara_job_list(self):
+ result = self.sahara('job-list')
+ jobs = self.parser.listing(result)
+ self.assertTableStruct(jobs, [
+ 'id',
+ 'cluster_id',
+ 'status'
+ ])
diff --git a/tempest/clients.py b/tempest/clients.py
index 6d27417..0ebbd7c 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -13,15 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
-# Default client libs
-import cinderclient.client
-import glanceclient
-import heatclient.client
import keystoneclient.exceptions
import keystoneclient.v2_0.client
-import neutronclient.v2_0.client
-import novaclient.client
-import swiftclient
from tempest.common.rest_client import NegativeRestClient
from tempest import config
@@ -30,6 +23,8 @@
from tempest.openstack.common import log as logging
from tempest.services.baremetal.v1.client_json import BaremetalClientJSON
from tempest.services import botoclients
+from tempest.services.compute.json.agents_client import \
+ AgentsClientJSON
from tempest.services.compute.json.aggregates_client import \
AggregatesClientJSON
from tempest.services.compute.json.availability_zone_client import \
@@ -52,6 +47,8 @@
InterfacesClientJSON
from tempest.services.compute.json.keypairs_client import KeyPairsClientJSON
from tempest.services.compute.json.limits_client import LimitsClientJSON
+from tempest.services.compute.json.migrations_client import \
+ MigrationsClientJSON
from tempest.services.compute.json.quotas_client import QuotasClientJSON
from tempest.services.compute.json.security_groups_client import \
SecurityGroupsClientJSON
@@ -370,6 +367,7 @@
# common clients
self.account_client = AccountClient(self.auth_provider)
+ self.agents_client = AgentsClientJSON(self.auth_provider)
if CONF.service_available.glance:
self.image_client = ImageClientJSON(self.auth_provider)
self.image_client_v2 = ImageClientV2JSON(self.auth_provider)
@@ -385,6 +383,7 @@
AccountClientCustomizedHeader(self.auth_provider)
self.data_processing_client = DataProcessingClient(
self.auth_provider)
+ self.migrations_client = MigrationsClientJSON(self.auth_provider)
class AltManager(Manager):
@@ -460,6 +459,8 @@
NOVACLIENT_VERSION = '2'
CINDERCLIENT_VERSION = '1'
HEATCLIENT_VERSION = '1'
+ IRONICCLIENT_VERSION = '1'
+ SAHARACLIENT_VERSION = '1.1'
def __init__(self, username, password, tenant_name):
# FIXME(andreaf) Auth provider for client_type 'official' is
@@ -469,6 +470,7 @@
# super cares for credentials validation
super(OfficialClientManager, self).__init__(
username=username, password=password, tenant_name=tenant_name)
+ self.baremetal_client = self._get_baremetal_client()
self.compute_client = self._get_compute_client(username,
password,
tenant_name)
@@ -488,11 +490,34 @@
username,
password,
tenant_name)
+ self.data_processing_client = self._get_data_processing_client(
+ username,
+ password,
+ tenant_name)
+
+ def _get_roles(self):
+ keystone_admin = self._get_identity_client(
+ CONF.identity.admin_username,
+ CONF.identity.admin_password,
+ CONF.identity.admin_tenant_name)
+
+ username = self.credentials['username']
+ tenant_name = self.credentials['tenant_name']
+ user_id = keystone_admin.users.find(name=username).id
+ tenant_id = keystone_admin.tenants.find(name=tenant_name).id
+
+ roles = keystone_admin.roles.roles_for_user(
+ user=user_id, tenant=tenant_id)
+
+ return [r.name for r in roles]
def _get_compute_client(self, username, password, tenant_name):
# Novaclient will not execute operations for anyone but the
# identified user, so a new client needs to be created for
# each user that operations need to be performed for.
+ if not CONF.service_available.nova:
+ return None
+ import novaclient.client
self._validate_credentials(username, password, tenant_name)
auth_url = CONF.identity.uri
@@ -514,6 +539,9 @@
http_log_debug=True)
def _get_image_client(self):
+ if not CONF.service_available.glance:
+ return None
+ import glanceclient
token = self.identity_client.auth_token
region = CONF.identity.region
endpoint_type = CONF.image.endpoint_type
@@ -525,9 +553,13 @@
insecure=dscv)
def _get_volume_client(self, username, password, tenant_name):
+ if not CONF.service_available.cinder:
+ return None
+ import cinderclient.client
auth_url = CONF.identity.uri
region = CONF.identity.region
endpoint_type = CONF.volume.endpoint_type
+ dscv = CONF.identity.disable_ssl_certificate_validation
return cinderclient.client.Client(self.CINDERCLIENT_VERSION,
username,
password,
@@ -535,9 +567,13 @@
auth_url,
region_name=region,
endpoint_type=endpoint_type,
+ insecure=dscv,
http_log_debug=True)
def _get_object_storage_client(self, username, password, tenant_name):
+ if not CONF.service_available.swift:
+ return None
+ import swiftclient
auth_url = CONF.identity.uri
# add current tenant to swift operator role group.
keystone_admin = self._get_identity_client(
@@ -567,6 +603,9 @@
def _get_orchestration_client(self, username=None, password=None,
tenant_name=None):
+ if not CONF.service_available.heat:
+ return None
+ import heatclient.client
if not username:
username = CONF.identity.admin_username
if not password:
@@ -610,6 +649,37 @@
auth_url=auth_url,
insecure=dscv)
+ def _get_baremetal_client(self):
+ # ironic client is currently intended to by used by admin users
+ if not CONF.service_available.ironic:
+ return None
+ import ironicclient.client
+ roles = self._get_roles()
+ if CONF.identity.admin_role not in roles:
+ return None
+
+ auth_url = CONF.identity.uri
+ api_version = self.IRONICCLIENT_VERSION
+ insecure = CONF.identity.disable_ssl_certificate_validation
+ service_type = CONF.baremetal.catalog_type
+ endpoint_type = CONF.baremetal.endpoint_type
+ creds = {
+ 'os_username': self.credentials['username'],
+ 'os_password': self.credentials['password'],
+ 'os_tenant_name': self.credentials['tenant_name']
+ }
+
+ try:
+ return ironicclient.client.get_client(
+ api_version=api_version,
+ os_auth_url=auth_url,
+ insecure=insecure,
+ os_service_type=service_type,
+ os_endpoint_type=endpoint_type,
+ **creds)
+ except keystoneclient.exceptions.EndpointNotFound:
+ return None
+
def _get_network_client(self):
# The intended configuration is for the network client to have
# admin privileges and indicate for whom resources are being
@@ -617,6 +687,9 @@
# preferable to authenticating as a specific user because
# working with certain resources (public routers and networks)
# often requires admin privileges anyway.
+ if not CONF.service_available.neutron:
+ return None
+ import neutronclient.v2_0.client
username = CONF.identity.admin_username
password = CONF.identity.admin_password
tenant_name = CONF.identity.admin_tenant_name
@@ -633,3 +706,25 @@
endpoint_type=endpoint_type,
auth_url=auth_url,
insecure=dscv)
+
+ def _get_data_processing_client(self, username, password, tenant_name):
+ if not CONF.service_available.sahara:
+ # Sahara isn't available
+ return None
+
+ import saharaclient.client
+
+ self._validate_credentials(username, password, tenant_name)
+
+ endpoint_type = CONF.data_processing.endpoint_type
+ catalog_type = CONF.data_processing.catalog_type
+ auth_url = CONF.identity.uri
+
+ client = saharaclient.client.Client(self.SAHARACLIENT_VERSION,
+ username, password,
+ project_name=tenant_name,
+ endpoint_type=endpoint_type,
+ service_type=catalog_type,
+ auth_url=auth_url)
+
+ return client
diff --git a/tempest/common/debug.py b/tempest/common/debug.py
index 6a496c2..228be7a 100644
--- a/tempest/common/debug.py
+++ b/tempest/common/debug.py
@@ -20,7 +20,7 @@
CONF = config.CONF
LOG = logging.getLogger(__name__)
-tables = ['filter', 'nat', 'mangle']
+TABLES = ['filter', 'nat', 'mangle']
def log_ip_ns():
@@ -28,14 +28,14 @@
return
LOG.info("Host Addr:\n" + commands.ip_addr_raw())
LOG.info("Host Route:\n" + commands.ip_route_raw())
- for table in ['filter', 'nat', 'mangle']:
+ for table in TABLES:
LOG.info('Host %s table:\n%s', table, commands.iptables_raw(table))
ns_list = commands.ip_ns_list()
LOG.info("Host ns list" + str(ns_list))
for ns in ns_list:
LOG.info("ns(%s) Addr:\n%s", ns, commands.ip_ns_addr(ns))
LOG.info("ns(%s) Route:\n%s", ns, commands.ip_ns_route(ns))
- for table in ['filter', 'nat', 'mangle']:
+ for table in TABLES:
LOG.info('ns(%s) table(%s):\n%s', ns, table,
commands.iptables_ns(ns, table))
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 23563b5..8c07d4f 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -197,26 +197,26 @@
details = pattern.format(read_code, expected_code)
raise exceptions.InvalidHttpSuccessCode(details)
- def post(self, url, body, headers=None):
- return self.request('POST', url, headers, body)
+ def post(self, url, body, headers=None, extra_headers=False):
+ return self.request('POST', url, extra_headers, headers, body)
- def get(self, url, headers=None):
- return self.request('GET', url, headers)
+ def get(self, url, headers=None, extra_headers=False):
+ return self.request('GET', url, extra_headers, headers)
- def delete(self, url, headers=None, body=None):
- return self.request('DELETE', url, headers, body)
+ def delete(self, url, headers=None, body=None, extra_headers=False):
+ return self.request('DELETE', url, extra_headers, headers, body)
- def patch(self, url, body, headers=None):
- return self.request('PATCH', url, headers, body)
+ def patch(self, url, body, headers=None, extra_headers=False):
+ return self.request('PATCH', url, extra_headers, headers, body)
- def put(self, url, body, headers=None):
- return self.request('PUT', url, headers, body)
+ def put(self, url, body, headers=None, extra_headers=False):
+ return self.request('PUT', url, extra_headers, headers, body)
- def head(self, url, headers=None):
- return self.request('HEAD', url, headers)
+ def head(self, url, headers=None, extra_headers=False):
+ return self.request('HEAD', url, extra_headers, headers)
- def copy(self, url, headers=None):
- return self.request('COPY', url, headers)
+ def copy(self, url, headers=None, extra_headers=False):
+ return self.request('COPY', url, extra_headers, headers)
def get_versions(self):
resp, body = self.get('')
@@ -418,13 +418,22 @@
return resp, resp_body
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
+ # if extra_headers is True
+ # default headers would be added to headers
retry = 0
if headers is None:
# NOTE(vponomaryov): if some client do not need headers,
# it should explicitly pass empty dict
headers = self.get_headers()
+ elif extra_headers:
+ try:
+ headers = headers.copy()
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = self.get_headers()
resp, resp_body = self._request(method, url,
headers=headers, body=body)
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index 00e5e0d..95b6833 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -112,3 +112,8 @@
def turn_nic_on(self, nic):
cmd = "sudo /bin/ip link set {nic} up".format(nic=nic)
return self.exec_command(cmd)
+
+ def get_pids(self, pr_name):
+ # Get pid(s) of a process/program
+ cmd = "ps -ef | grep %s | grep -v 'grep' | awk {'print $1'}" % pr_name
+ return self.exec_command(cmd).split('\n')
diff --git a/tempest/config.py b/tempest/config.py
index 0212d8a..b9fe572 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -126,7 +126,7 @@
"OpenStack Identity API admin credentials are known."),
cfg.StrOpt('image_ref',
default="{$IMAGE_ID}",
- help="Valid secondary image reference to be used in tests."),
+ help="Valid primary image reference to be used in tests."),
cfg.StrOpt('image_ref_alt',
default="{$IMAGE_ID_ALT}",
help="Valid secondary image reference to be used in tests."),
@@ -441,6 +441,9 @@
cfg.StrOpt('disk_format',
default='raw',
help='Disk format to use when copying a volume to image'),
+ cfg.IntOpt('volume_size',
+ default=1,
+ help='Default size in GB for volumes created by volumes tests'),
]
volume_feature_group = cfg.OptGroup(name='volume-feature-enabled',
@@ -556,7 +559,7 @@
default=1,
help="Time in seconds between build status checks."),
cfg.IntOpt('build_timeout',
- default=600,
+ default=1200,
help="Timeout in seconds to wait for a stack to build."),
cfg.StrOpt('instance_type',
default='m1.micro',
@@ -844,13 +847,29 @@
BaremetalGroup = [
cfg.StrOpt('catalog_type',
default='baremetal',
- help="Catalog type of the baremetal provisioning service."),
+ help="Catalog type of the baremetal provisioning service"),
+ cfg.BoolOpt('driver_enabled',
+ default=False,
+ help="Whether the Ironic nova-compute driver is enabled"),
cfg.StrOpt('endpoint_type',
default='publicURL',
choices=['public', 'admin', 'internal',
'publicURL', 'adminURL', 'internalURL'],
help="The endpoint type to use for the baremetal provisioning "
- "service."),
+ "service"),
+ cfg.IntOpt('active_timeout',
+ default=300,
+ help="Timeout for Ironic node to completely provision"),
+ cfg.IntOpt('association_timeout',
+ default=10,
+ help="Timeout for association of Nova instance and Ironic "
+ "node"),
+ cfg.IntOpt('power_timeout',
+ default=20,
+ help="Timeout for Ironic power transitions."),
+ cfg.IntOpt('unprovision_timeout',
+ default=20,
+ help="Timeout for unprovisioning an Ironic node.")
]
cli_group = cfg.OptGroup(name='cli', title="cli Configuration Options")
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
new file mode 100644
index 0000000..857e1e8
--- /dev/null
+++ b/tempest/exceptions.py
@@ -0,0 +1,209 @@
+# 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
+
+
+class TempestException(Exception):
+ """
+ Base Tempest Exception
+
+ To correctly use this class, inherit from it and define
+ a 'message' property. That message will get printf'd
+ with the keyword arguments provided to the constructor.
+ """
+ message = "An unknown exception occurred"
+
+ def __init__(self, *args, **kwargs):
+ super(TempestException, self).__init__()
+ try:
+ self._error_string = self.message % kwargs
+ except Exception:
+ # at least get the core message out if something happened
+ self._error_string = self.message
+ if len(args) > 0:
+ # If there is a non-kwarg parameter, assume it's the error
+ # message or reason description and tack it on to the end
+ # of the exception message
+ # Convert all arguments into their string representations...
+ args = ["%s" % arg for arg in args]
+ self._error_string = (self._error_string +
+ "\nDetails: %s" % '\n'.join(args))
+
+ def __str__(self):
+ return self._error_string
+
+
+class RestClientException(TempestException,
+ testtools.TestCase.failureException):
+ pass
+
+
+class RFCViolation(RestClientException):
+ message = "RFC Violation"
+
+
+class InvalidConfiguration(TempestException):
+ message = "Invalid Configuration"
+
+
+class InvalidCredentials(TempestException):
+ message = "Invalid Credentials"
+
+
+class InvalidHttpSuccessCode(RestClientException):
+ message = "The success code is different than the expected one"
+
+
+class NotFound(RestClientException):
+ message = "Object not found"
+
+
+class Unauthorized(RestClientException):
+ message = 'Unauthorized'
+
+
+class InvalidServiceTag(RestClientException):
+ message = "Invalid service tag"
+
+
+class TimeoutException(TempestException):
+ message = "Request timed out"
+
+
+class BuildErrorException(TempestException):
+ message = "Server %(server_id)s failed to build and is in ERROR status"
+
+
+class ImageKilledException(TempestException):
+ message = "Image %(image_id)s 'killed' while waiting for '%(status)s'"
+
+
+class AddImageException(TempestException):
+ message = "Image %(image_id)s failed to become ACTIVE in the allotted time"
+
+
+class EC2RegisterImageException(TempestException):
+ message = ("Image %(image_id)s failed to become 'available' "
+ "in the allotted time")
+
+
+class VolumeBuildErrorException(TempestException):
+ message = "Volume %(volume_id)s failed to build and is in ERROR status"
+
+
+class SnapshotBuildErrorException(TempestException):
+ message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
+
+
+class VolumeBackupException(TempestException):
+ message = "Volume backup %(backup_id)s failed and is in ERROR status"
+
+
+class StackBuildErrorException(TempestException):
+ message = ("Stack %(stack_identifier)s is in %(stack_status)s status "
+ "due to '%(stack_status_reason)s'")
+
+
+class StackResourceBuildErrorException(TempestException):
+ message = ("Resource %(resource_name) in stack %(stack_identifier)s is "
+ "in %(resource_status)s status due to "
+ "'%(resource_status_reason)s'")
+
+
+class BadRequest(RestClientException):
+ message = "Bad request"
+
+
+class UnprocessableEntity(RestClientException):
+ message = "Unprocessable entity"
+
+
+class AuthenticationFailure(RestClientException):
+ message = ("Authentication with user %(user)s and password "
+ "%(password)s failed auth using tenant %(tenant)s.")
+
+
+class EndpointNotFound(TempestException):
+ message = "Endpoint not found"
+
+
+class RateLimitExceeded(TempestException):
+ message = "Rate limit exceeded"
+
+
+class OverLimit(TempestException):
+ message = "Quota exceeded"
+
+
+class ServerFault(TempestException):
+ message = "Got server fault"
+
+
+class ImageFault(TempestException):
+ message = "Got image fault"
+
+
+class IdentityError(TempestException):
+ message = "Got identity error"
+
+
+class Conflict(RestClientException):
+ message = "An object with that identifier already exists"
+
+
+class SSHTimeout(TempestException):
+ message = ("Connection to the %(host)s via SSH timed out.\n"
+ "User: %(user)s, Password: %(password)s")
+
+
+class SSHExecCommandFailed(TempestException):
+ """Raised when remotely executed command returns nonzero status."""
+ message = ("Command '%(command)s', exit status: %(exit_status)d, "
+ "Error:\n%(strerror)s")
+
+
+class ServerUnreachable(TempestException):
+ message = "The server is not reachable via the configured network"
+
+
+class TearDownException(TempestException):
+ message = "%(num)d cleanUp operation failed"
+
+
+class ResponseWithNonEmptyBody(RFCViolation):
+ message = ("RFC Violation! Response with %(status)d HTTP Status Code "
+ "MUST NOT have a body")
+
+
+class ResponseWithEntity(RFCViolation):
+ message = ("RFC Violation! Response with 205 HTTP Status Code "
+ "MUST NOT have an entity")
+
+
+class InvalidHTTPResponseBody(RestClientException):
+ message = "HTTP response body is invalid json or xml"
+
+
+class InvalidContentType(RestClientException):
+ message = "Invalid content type provided"
+
+
+class UnexpectedResponseCode(RestClientException):
+ message = "Unexpected response code received"
+
+
+class InvalidStructure(TempestException):
+ message = "Invalid structure of table with details"
diff --git a/tempest/exceptions/README.rst b/tempest/exceptions/README.rst
deleted file mode 100644
index dbe42b2..0000000
--- a/tempest/exceptions/README.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-Tempest Field Guide to Exceptions
-=================================
-
-
-What are these exceptions?
---------------------------
-
-These exceptions are used by Tempest for covering OpenStack specific exceptional
-cases.
-
-How to add new exceptions?
---------------------------
-
-Each exception-template for inheritance purposes should be added into 'base'
-submodule.
-All other exceptions can be added in two ways:
-- in main module
-- in submodule
-But only in one of the ways. Need to make sure, that new exception is not
-present already.
-
-How to use exceptions?
-----------------------
-
-Any exceptions from this module or its submodules should be used in appropriate
-places to handle exceptional cases.
-Classes from 'base' module should be used only for inheritance.
diff --git a/tempest/exceptions/__init__.py b/tempest/exceptions/__init__.py
deleted file mode 100644
index d313def..0000000
--- a/tempest/exceptions/__init__.py
+++ /dev/null
@@ -1,164 +0,0 @@
-# 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.exceptions import base
-
-
-class InvalidConfiguration(base.TempestException):
- message = "Invalid Configuration"
-
-
-class InvalidCredentials(base.TempestException):
- message = "Invalid Credentials"
-
-
-class InvalidHttpSuccessCode(base.RestClientException):
- message = "The success code is different than the expected one"
-
-
-class NotFound(base.RestClientException):
- message = "Object not found"
-
-
-class Unauthorized(base.RestClientException):
- message = 'Unauthorized'
-
-
-class InvalidServiceTag(base.RestClientException):
- message = "Invalid service tag"
-
-
-class TimeoutException(base.TempestException):
- message = "Request timed out"
-
-
-class BuildErrorException(base.TempestException):
- message = "Server %(server_id)s failed to build and is in ERROR status"
-
-
-class ImageKilledException(base.TempestException):
- message = "Image %(image_id)s 'killed' while waiting for '%(status)s'"
-
-
-class AddImageException(base.TempestException):
- message = "Image %(image_id)s failed to become ACTIVE in the allotted time"
-
-
-class EC2RegisterImageException(base.TempestException):
- message = ("Image %(image_id)s failed to become 'available' "
- "in the allotted time")
-
-
-class VolumeBuildErrorException(base.TempestException):
- message = "Volume %(volume_id)s failed to build and is in ERROR status"
-
-
-class SnapshotBuildErrorException(base.TempestException):
- message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
-
-
-class VolumeBackupException(base.TempestException):
- message = "Volume backup %(backup_id)s failed and is in ERROR status"
-
-
-class StackBuildErrorException(base.TempestException):
- message = ("Stack %(stack_identifier)s is in %(stack_status)s status "
- "due to '%(stack_status_reason)s'")
-
-
-class BadRequest(base.RestClientException):
- message = "Bad request"
-
-
-class UnprocessableEntity(base.RestClientException):
- message = "Unprocessable entity"
-
-
-class AuthenticationFailure(base.RestClientException):
- message = ("Authentication with user %(user)s and password "
- "%(password)s failed auth using tenant %(tenant)s.")
-
-
-class EndpointNotFound(base.TempestException):
- message = "Endpoint not found"
-
-
-class RateLimitExceeded(base.TempestException):
- message = "Rate limit exceeded"
-
-
-class OverLimit(base.TempestException):
- message = "Quota exceeded"
-
-
-class ServerFault(base.TempestException):
- message = "Got server fault"
-
-
-class ImageFault(base.TempestException):
- message = "Got image fault"
-
-
-class IdentityError(base.TempestException):
- message = "Got identity error"
-
-
-class Conflict(base.RestClientException):
- message = "An object with that identifier already exists"
-
-
-class SSHTimeout(base.TempestException):
- message = ("Connection to the %(host)s via SSH timed out.\n"
- "User: %(user)s, Password: %(password)s")
-
-
-class SSHExecCommandFailed(base.TempestException):
- """Raised when remotely executed command returns nonzero status."""
- message = ("Command '%(command)s', exit status: %(exit_status)d, "
- "Error:\n%(strerror)s")
-
-
-class ServerUnreachable(base.TempestException):
- message = "The server is not reachable via the configured network"
-
-
-class TearDownException(base.TempestException):
- message = "%(num)d cleanUp operation failed"
-
-
-class ResponseWithNonEmptyBody(base.RFCViolation):
- message = ("RFC Violation! Response with %(status)d HTTP Status Code "
- "MUST NOT have a body")
-
-
-class ResponseWithEntity(base.RFCViolation):
- message = ("RFC Violation! Response with 205 HTTP Status Code "
- "MUST NOT have an entity")
-
-
-class InvalidHTTPResponseBody(base.RestClientException):
- message = "HTTP response body is invalid json or xml"
-
-
-class InvalidContentType(base.RestClientException):
- message = "Invalid content type provided"
-
-
-class UnexpectedResponseCode(base.RestClientException):
- message = "Unexpected response code received"
-
-
-class InvalidStructure(base.TempestException):
- message = "Invalid structure of table with details"
diff --git a/tempest/exceptions/base.py b/tempest/exceptions/base.py
deleted file mode 100644
index b8e470e..0000000
--- a/tempest/exceptions/base.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# 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
-
-
-class TempestException(Exception):
- """
- Base Tempest Exception
-
- To correctly use this class, inherit from it and define
- a 'message' property. That message will get printf'd
- with the keyword arguments provided to the constructor.
- """
- message = "An unknown exception occurred"
-
- def __init__(self, *args, **kwargs):
- super(TempestException, self).__init__()
- try:
- self._error_string = self.message % kwargs
- except Exception:
- # at least get the core message out if something happened
- self._error_string = self.message
- if len(args) > 0:
- # If there is a non-kwarg parameter, assume it's the error
- # message or reason description and tack it on to the end
- # of the exception message
- # Convert all arguments into their string representations...
- args = ["%s" % arg for arg in args]
- self._error_string = (self._error_string +
- "\nDetails: %s" % '\n'.join(args))
-
- def __str__(self):
- return self._error_string
-
-
-class RestClientException(TempestException,
- testtools.TestCase.failureException):
- pass
-
-
-class RFCViolation(RestClientException):
- message = "RFC Violation"
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index 234faad..270851d 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -47,7 +47,7 @@
T104: Scenario tests require a services decorator
"""
- if 'tempest/scenario' in filename:
+ if 'tempest/scenario/test_' in filename:
if TEST_DEFINITION.match(physical_line):
if not SCENARIO_DECORATOR.match(previous_logical):
return (physical_line.find('def'),
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index f06a850..5895c37 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -19,6 +19,7 @@
import six
import subprocess
+from ironicclient import exc as ironic_exceptions
import netaddr
from neutronclient.common import exceptions as exc
from novaclient import exceptions as nova_exceptions
@@ -71,11 +72,13 @@
username, password, tenant_name)
cls.compute_client = cls.manager.compute_client
cls.image_client = cls.manager.image_client
+ cls.baremetal_client = cls.manager.baremetal_client
cls.identity_client = cls.manager.identity_client
cls.network_client = cls.manager.network_client
cls.volume_client = cls.manager.volume_client
cls.object_storage_client = cls.manager.object_storage_client
cls.orchestration_client = cls.manager.orchestration_client
+ cls.data_processing_client = cls.manager.data_processing_client
cls.resource_keys = {}
cls.os_resources = []
@@ -283,7 +286,7 @@
return rules
def create_server(self, client=None, name=None, image=None, flavor=None,
- create_kwargs={}):
+ wait=True, create_kwargs={}):
if client is None:
client = self.compute_client
if name is None:
@@ -318,7 +321,8 @@
server = client.servers.create(name, image, flavor, **create_kwargs)
self.assertEqual(server.name, name)
self.set_resource(name, server)
- self.status_timeout(client.servers, server.id, 'ACTIVE')
+ if wait:
+ self.status_timeout(client.servers, server.id, 'ACTIVE')
# The instance retrieved on creation is missing network
# details, necessitating retrieval after it becomes active to
# ensure correct details.
@@ -439,6 +443,80 @@
LOG.debug("image:%s" % self.image)
+class BaremetalScenarioTest(OfficialClientTest):
+ @classmethod
+ def setUpClass(cls):
+ super(BaremetalScenarioTest, cls).setUpClass()
+
+ if (not CONF.service_available.ironic or
+ not CONF.baremetal.driver_enabled):
+ msg = 'Ironic not available or Ironic compute driver not enabled'
+ raise cls.skipException(msg)
+
+ # use an admin client manager for baremetal client
+ username, password, tenant = cls.admin_credentials()
+ manager = clients.OfficialClientManager(username, password, tenant)
+ cls.baremetal_client = manager.baremetal_client
+
+ # allow any issues obtaining the node list to raise early
+ cls.baremetal_client.node.list()
+
+ def _node_state_timeout(self, node_id, state_attr,
+ target_states, timeout=10, interval=1):
+ if not isinstance(target_states, list):
+ target_states = [target_states]
+
+ def check_state():
+ node = self.get_node(node_id=node_id)
+ if getattr(node, state_attr) in target_states:
+ return True
+ return False
+
+ if not tempest.test.call_until_true(
+ check_state, timeout, interval):
+ msg = ("Timed out waiting for node %s to reach %s state(s) %s" %
+ (node_id, state_attr, target_states))
+ raise exceptions.TimeoutException(msg)
+
+ def wait_provisioning_state(self, node_id, state, timeout):
+ self._node_state_timeout(
+ node_id=node_id, state_attr='provision_state',
+ target_states=state, timeout=timeout)
+
+ def wait_power_state(self, node_id, state):
+ self._node_state_timeout(
+ node_id=node_id, state_attr='power_state',
+ target_states=state, timeout=CONF.baremetal.power_timeout)
+
+ def wait_node(self, instance_id):
+ """Waits for a node to be associated with instance_id."""
+ def _get_node():
+ node = None
+ try:
+ node = self.get_node(instance_id=instance_id)
+ except ironic_exceptions.HTTPNotFound:
+ pass
+ return node is not None
+
+ if not tempest.test.call_until_true(
+ _get_node, CONF.baremetal.association_timeout, 1):
+ msg = ('Timed out waiting to get Ironic node by instance id %s'
+ % instance_id)
+ raise exceptions.TimeoutException(msg)
+
+ def get_node(self, node_id=None, instance_id=None):
+ if node_id:
+ return self.baremetal_client.node.get(node_id)
+ elif instance_id:
+ return self.baremetal_client.node.get_by_instance_uuid(instance_id)
+
+ def get_ports(self, node_id):
+ ports = []
+ for port in self.baremetal_client.node.list_ports(node_id):
+ ports.append(self.baremetal_client.port.get(port.uuid))
+ return ports
+
+
class NetworkScenarioTest(OfficialClientTest):
"""
Base class for network scenario tests
diff --git a/tempest/scenario/test_baremetal_basic_ops.py b/tempest/scenario/test_baremetal_basic_ops.py
new file mode 100644
index 0000000..c53aa83
--- /dev/null
+++ b/tempest/scenario/test_baremetal_basic_ops.py
@@ -0,0 +1,147 @@
+#
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# 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
+from tempest.openstack.common import log as logging
+from tempest.scenario import manager
+from tempest import test
+
+CONF = config.CONF
+
+LOG = logging.getLogger(__name__)
+
+
+# power/provision states as of icehouse
+class PowerStates(object):
+ """Possible power states of an Ironic node."""
+ POWER_ON = 'power on'
+ POWER_OFF = 'power off'
+ REBOOT = 'rebooting'
+ SUSPEND = 'suspended'
+
+
+class ProvisionStates(object):
+ """Possible provision states of an Ironic node."""
+ NOSTATE = None
+ INIT = 'initializing'
+ ACTIVE = 'active'
+ BUILDING = 'building'
+ DEPLOYWAIT = 'wait call-back'
+ DEPLOYING = 'deploying'
+ DEPLOYFAIL = 'deploy failed'
+ DEPLOYDONE = 'deploy complete'
+ DELETING = 'deleting'
+ DELETED = 'deleted'
+ ERROR = 'error'
+
+
+class BaremetalBasicOptsPXESSH(manager.BaremetalScenarioTest):
+ """
+ This smoke test tests the pxe_ssh Ironic driver. It follows this basic
+ set of operations:
+ * Creates a keypair
+ * Boots an instance using the keypair
+ * Monitors the associated Ironic node for power and
+ expected state transitions
+ * Validates Ironic node's driver_info has been properly
+ updated
+ * Validates Ironic node's port data has been properly updated
+ * Verifies SSH connectivity using created keypair via fixed IP
+ * Associates a floating ip
+ * Verifies SSH connectivity using created keypair via floating IP
+ * Deletes instance
+ * Monitors the associated Ironic node for power and
+ expected state transitions
+ """
+ def add_keypair(self):
+ self.keypair = self.create_keypair()
+
+ def add_floating_ip(self):
+ floating_ip = self.compute_client.floating_ips.create()
+ self.instance.add_floating_ip(floating_ip)
+ return floating_ip.ip
+
+ def verify_connectivity(self, ip=None):
+ if ip:
+ dest = self.get_remote_client(ip)
+ else:
+ dest = self.get_remote_client(self.instance)
+ dest.validate_authentication()
+
+ def validate_driver_info(self):
+ f_id = self.instance.flavor['id']
+ flavor_extra = self.compute_client.flavors.get(f_id).get_keys()
+ driver_info = self.node.driver_info
+ self.assertEqual(driver_info['pxe_deploy_kernel'],
+ flavor_extra['baremetal:deploy_kernel_id'])
+ self.assertEqual(driver_info['pxe_deploy_ramdisk'],
+ flavor_extra['baremetal:deploy_ramdisk_id'])
+ self.assertEqual(driver_info['pxe_image_source'],
+ self.instance.image['id'])
+
+ def validate_ports(self):
+ for port in self.get_ports(self.node.uuid):
+ n_port_id = port.extra['vif_port_id']
+ n_port = self.network_client.show_port(n_port_id)['port']
+ self.assertEqual(n_port['device_id'], self.instance.id)
+ self.assertEqual(n_port['mac_address'], port.address)
+
+ def boot_instance(self):
+ create_kwargs = {
+ 'key_name': self.keypair.id
+ }
+ self.instance = self.create_server(
+ wait=False, create_kwargs=create_kwargs)
+
+ self.set_resource('instance', self.instance)
+
+ self.wait_node(self.instance.id)
+ self.node = self.get_node(instance_id=self.instance.id)
+
+ self.wait_power_state(self.node.uuid, PowerStates.POWER_ON)
+
+ self.wait_provisioning_state(
+ self.node.uuid,
+ [ProvisionStates.DEPLOYWAIT, ProvisionStates.ACTIVE],
+ timeout=15)
+
+ self.wait_provisioning_state(self.node.uuid, ProvisionStates.ACTIVE,
+ timeout=CONF.baremetal.active_timeout)
+
+ self.status_timeout(
+ self.compute_client.servers, self.instance.id, 'ACTIVE')
+
+ self.node = self.get_node(instance_id=self.instance.id)
+ self.instance = self.compute_client.servers.get(self.instance.id)
+
+ def terminate_instance(self):
+ self.instance.delete()
+ self.remove_resource('instance')
+ self.wait_power_state(self.node.uuid, PowerStates.POWER_OFF)
+ self.wait_provisioning_state(
+ self.node.uuid,
+ ProvisionStates.NOSTATE,
+ timeout=CONF.baremetal.unprovision_timeout)
+
+ @test.services('baremetal', 'compute', 'image', 'network')
+ def test_baremetal_server_ops(self):
+ self.add_keypair()
+ self.boot_instance()
+ self.validate_driver_info()
+ self.validate_ports()
+ self.verify_connectivity()
+ floating_ip = self.add_floating_ip()
+ self.verify_connectivity(ip=floating_ip)
+ self.terminate_instance()
diff --git a/tempest/scenario/test_load_balancer_basic.py b/tempest/scenario/test_load_balancer_basic.py
index 1092b94..5f71461 100644
--- a/tempest/scenario/test_load_balancer_basic.py
+++ b/tempest/scenario/test_load_balancer_basic.py
@@ -17,7 +17,6 @@
import urllib
from tempest.api.network import common as net_common
-from tempest.common.utils import data_utils
from tempest import config
from tempest import exceptions
from tempest.scenario import manager
@@ -59,20 +58,27 @@
def setUpClass(cls):
super(TestLoadBalancerBasic, cls).setUpClass()
cls.check_preconditions()
- cls.security_groups = {}
cls.servers_keypairs = {}
cls.members = []
cls.floating_ips = {}
- cls.server_ip = None
- cls.vip_ip = None
+ cls.server_ips = {}
cls.port1 = 80
cls.port2 = 88
- def _create_security_groups(self):
- self.security_groups[self.tenant_id] =\
- self._create_security_group_neutron(tenant_id=self.tenant_id)
+ def setUp(self):
+ super(TestLoadBalancerBasic, self).setUp()
+ self.server_ips = {}
+ self._create_security_group()
+
+ def cleanup_wrapper(self, resource):
+ self.cleanup_resource(resource, self.__class__.__name__)
+
+ def _create_security_group(self):
+ self.security_group = self._create_security_group_neutron(
+ tenant_id=self.tenant_id)
self._create_security_group_rules_for_port(self.port1)
self._create_security_group_rules_for_port(self.port2)
+ self.addCleanup(self.cleanup_wrapper, self.security_group)
def _create_security_group_rules_for_port(self, port):
rule = {
@@ -83,15 +89,14 @@
}
self._create_security_group_rule(
client=self.network_client,
- secgroup=self.security_groups[self.tenant_id],
+ secgroup=self.security_group,
tenant_id=self.tenant_id,
**rule)
- def _create_server(self):
- tenant_id = self.tenant_id
- name = data_utils.rand_name("smoke_server-")
+ def _create_server(self, name):
keypair = self.create_keypair(name='keypair-%s' % name)
- security_groups = [self.security_groups[tenant_id].name]
+ self.addCleanup(self.cleanup_wrapper, keypair)
+ security_groups = [self.security_group.name]
net = self._list_networks(tenant_id=self.tenant_id)[0]
create_kwargs = {
'nics': [
@@ -102,51 +107,106 @@
}
server = self.create_server(name=name,
create_kwargs=create_kwargs)
- self.servers_keypairs[server] = keypair
+ self.addCleanup(self.cleanup_wrapper, server)
+ self.servers_keypairs[server.id] = keypair
if (config.network.public_network_id and not
config.network.tenant_networks_reachable):
public_network_id = config.network.public_network_id
floating_ip = self._create_floating_ip(
server, public_network_id)
+ self.addCleanup(self.cleanup_wrapper, floating_ip)
self.floating_ips[floating_ip] = server
- self.server_ip = floating_ip.floating_ip_address
+ self.server_ips[server.id] = floating_ip.floating_ip_address
else:
- self.server_ip = server.networks[net['name']][0]
+ self.server_ips[server.id] = server.networks[net.name][0]
self.assertTrue(self.servers_keypairs)
return server
- def _start_servers(self, server):
+ def _create_servers(self):
+ for count in range(2):
+ self._create_server(name=("server%s" % (count + 1)))
+ self.assertEqual(len(self.servers_keypairs), 2)
+
+ def _start_servers(self):
"""
+ Start two backends
+
1. SSH to the instance
2. Start two http backends listening on ports 80 and 88 respectively
+ In case there are two instances, each backend is created on a separate
+ instance.
+
+ The backends are the inetd services. To start them we need to edit
+ /etc/inetd.conf in the following way:
+ www stream tcp nowait root /bin/sh sh /home/cirros/script_name
+
+ Where /home/cirros/script_name is a path to a script which
+ echoes the responses:
+ echo -e 'HTTP/1.0 200 OK\r\n\r\nserver_name
+
+ If we want the server to listen on port 88, then we use
+ "kerberos" instead of "www".
"""
- private_key = self.servers_keypairs[server].private_key
- ssh_client = self.get_remote_client(
- server_or_ip=self.server_ip,
- private_key=private_key).ssh_client
- start_server = "while true; do echo -e 'HTTP/1.0 200 OK\r\n\r\n" \
- "%(server)s' | sudo nc -l -p %(port)s ; done &"
- cmd = start_server % {'server': 'server1',
- 'port': self.port1}
- ssh_client.exec_command(cmd)
- cmd = start_server % {'server': 'server2',
- 'port': self.port2}
- ssh_client.exec_command(cmd)
+ for server_id, ip in self.server_ips.iteritems():
+ private_key = self.servers_keypairs[server_id].private_key
+ server_name = self.compute_client.servers.get(server_id).name
+ ssh_client = self.get_remote_client(
+ server_or_ip=ip,
+ private_key=private_key)
+ ssh_client.validate_authentication()
+ # Create service for inetd
+ create_script = """sudo sh -c "echo -e \\"echo -e 'HTTP/1.0 """ \
+ """200 OK\\\\\\r\\\\\\n\\\\\\r\\\\\\n""" \
+ """%(server)s'\\" >>/home/cirros/%(script)s\""""
- def _check_connection(self, check_ip):
- def try_connect(ip):
+ cmd = create_script % {
+ 'server': server_name,
+ 'script': 'script1'}
+ ssh_client.exec_command(cmd)
+ # Configure inetd
+ configure_inetd = """sudo sh -c "echo -e \\"%(service)s """ \
+ """stream tcp nowait root /bin/sh sh """ \
+ """/home/cirros/%(script)s\\" >> """ \
+ """/etc/inetd.conf\""""
+ # "www" stands for port 80
+ cmd = configure_inetd % {'service': 'www',
+ 'script': 'script1'}
+ ssh_client.exec_command(cmd)
+
+ if len(self.server_ips) == 1:
+ cmd = create_script % {'server': 'server2',
+ 'script': 'script2'}
+ ssh_client.exec_command(cmd)
+ # "kerberos" stands for port 88
+ cmd = configure_inetd % {'service': 'kerberos',
+ 'script': 'script2'}
+ ssh_client.exec_command(cmd)
+
+ # Get PIDs of inetd
+ pids = ssh_client.get_pids('inetd')
+ if pids != ['']:
+ # If there are any inetd processes, reload them
+ kill_cmd = "sudo kill -HUP %s" % ' '.join(pids)
+ ssh_client.exec_command(kill_cmd)
+ else:
+ # In other case start inetd
+ start_inetd = "sudo /usr/sbin/inetd /etc/inetd.conf"
+ ssh_client.exec_command(start_inetd)
+
+ def _check_connection(self, check_ip, port=80):
+ def try_connect(ip, port):
try:
- urllib.urlopen("http://{0}/".format(ip))
- return True
+ resp = urllib.urlopen("http://{0}:{1}/".format(ip, port))
+ if resp.getcode() == 200:
+ return True
+ return False
except IOError:
return False
timeout = config.compute.ping_timeout
- timer = 0
- while not try_connect(check_ip):
- time.sleep(1)
- timer += 1
- if timer >= timeout:
+ start = time.time()
+ while not try_connect(check_ip, port):
+ if (time.time() - start) > timeout:
message = "Timed out trying to connect to %s" % check_ip
raise exceptions.TimeoutException(message)
@@ -157,30 +217,37 @@
self.subnet = net_common.DeletableSubnet(client=self.network_client,
**subnet)
self.pool = super(TestLoadBalancerBasic, self)._create_pool(
- 'ROUND_ROBIN',
- 'HTTP',
- self.subnet.id)
+ lb_method='ROUND_ROBIN',
+ protocol='HTTP',
+ subnet_id=self.subnet.id)
+ self.addCleanup(self.cleanup_wrapper, self.pool)
self.assertTrue(self.pool)
- def _create_members(self, server_ids):
+ def _create_members(self):
"""
Create two members.
In case there is only one server, create both members with the same ip
but with different ports to listen on.
"""
- servers = self.compute_client.servers.list()
- for server in servers:
- if server.id in server_ids:
- ip = self.server_ip
- pool_id = self.pool.id
- if len(set(server_ids)) == 1 or len(servers) == 1:
- member1 = self._create_member(ip, self.port1, pool_id)
- member2 = self._create_member(ip, self.port2, pool_id)
- self.members.extend([member1, member2])
- else:
- member = self._create_member(ip, self.port1, pool_id)
- self.members.append(member)
+
+ for server_id, ip in self.server_ips.iteritems():
+ if len(self.server_ips) == 1:
+ member1 = self._create_member(address=ip,
+ protocol_port=self.port1,
+ pool_id=self.pool.id)
+ self.addCleanup(self.cleanup_wrapper, member1)
+ member2 = self._create_member(address=ip,
+ protocol_port=self.port2,
+ pool_id=self.pool.id)
+ self.addCleanup(self.cleanup_wrapper, member2)
+ self.members.extend([member1, member2])
+ else:
+ member = self._create_member(address=ip,
+ protocol_port=self.port1,
+ pool_id=self.pool.id)
+ self.addCleanup(self.cleanup_wrapper, member)
+ self.members.append(member)
self.assertTrue(self.members)
def _assign_floating_ip_to_vip(self, vip):
@@ -188,22 +255,23 @@
port_id = vip.port_id
floating_ip = self._create_floating_ip(vip, public_network_id,
port_id=port_id)
+ self.addCleanup(self.cleanup_wrapper, floating_ip)
self.floating_ips.setdefault(vip.id, [])
self.floating_ips[vip.id].append(floating_ip)
def _create_load_balancer(self):
self._create_pool()
- self._create_members([self.servers_keypairs.keys()[0].id])
- subnet_id = self.subnet.id
- pool_id = self.pool.id
- self.vip = super(TestLoadBalancerBasic, self)._create_vip('HTTP', 80,
- subnet_id,
- pool_id)
- self._status_timeout(NeutronRetriever(self.network_client,
- self.network_client.vip_path,
- net_common.DeletableVip),
- self.vip.id,
- expected_status='ACTIVE')
+ self._create_members()
+ self.vip = self._create_vip(protocol='HTTP',
+ protocol_port=80,
+ subnet_id=self.subnet.id,
+ pool_id=self.pool.id)
+ self.addCleanup(self.cleanup_wrapper, self.vip)
+ self.status_timeout(NeutronRetriever(self.network_client,
+ self.network_client.vip_path,
+ net_common.DeletableVip),
+ self.vip.id,
+ expected_status='ACTIVE')
if (config.network.public_network_id and not
config.network.tenant_networks_reachable):
self._assign_floating_ip_to_vip(self.vip)
@@ -214,33 +282,49 @@
def _check_load_balancing(self):
"""
- 1. Send 10 requests on the floating ip associated with the VIP
+ 1. Send 100 requests on the floating ip associated with the VIP
2. Check that the requests are shared between
the two servers and that both of them get equal portions
of the requests
"""
self._check_connection(self.vip_ip)
+ resp = self._send_requests(self.vip_ip)
+ self.assertEqual(set(["server1\n", "server2\n"]), set(resp))
+ self.assertEqual(50, resp.count("server1\n"))
+ self.assertEqual(50, resp.count("server2\n"))
+
+ def _send_requests(self, vip_ip):
resp = []
- for count in range(10):
+ for count in range(100):
resp.append(
urllib.urlopen(
- "http://{0}/".format(self.vip_ip)).read())
- self.assertEqual(set(["server1\n", "server2\n"]), set(resp))
- self.assertEqual(5, resp.count("server1\n"))
- self.assertEqual(5, resp.count("server2\n"))
+ "http://{0}/".format(vip_ip)).read())
+ return resp
@test.attr(type='smoke')
@test.services('compute', 'network')
def test_load_balancer_basic(self):
- self._create_security_groups()
- server = self._create_server()
- self._start_servers(server)
+ self._create_server('server1')
+ self._start_servers()
self._create_load_balancer()
self._check_load_balancing()
class NeutronRetriever(object):
+ """
+ Helper class to make possible handling neutron objects returned by GET
+ requests as attribute dicts.
+
+ Whet get() method is called, the returned dictionary is wrapped into
+ a corresponding DeletableResource class which provides attribute access
+ to dictionary values.
+
+ Usage:
+ This retriever is used to allow using status_timeout from
+ tempest.manager with Neutron objects.
+ """
+
def __init__(self, network_client, path, resource):
self.network_client = network_client
self.path = path
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index e7e97b5..0ba65cf 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.common import debug
from tempest.common.utils import data_utils
from tempest import config
@@ -158,6 +160,8 @@
self.server.rebuild(image_ref_alt)
self._wait_server_status_and_check_network_connectivity()
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@services('compute', 'network')
def test_server_connectivity_pause_unpause(self):
self.server.pause()
@@ -167,6 +171,8 @@
self.server.unpause()
self._wait_server_status_and_check_network_connectivity()
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@services('compute', 'network')
def test_server_connectivity_suspend_resume(self):
self.server.suspend()
@@ -176,11 +182,10 @@
self.server.resume()
self._wait_server_status_and_check_network_connectivity()
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize is not available.')
@services('compute', 'network')
def test_server_connectivity_resize(self):
- if not CONF.compute_feature_enabled.resize:
- msg = "Skipping test - resize not available on this host"
- raise self.skipException(msg)
resize_flavor = CONF.compute.flavor_ref_alt
if resize_flavor == CONF.compute.flavor_ref:
msg = "Skipping test - flavor_ref and flavor_ref_alt are identical"
diff --git a/tempest/scenario/test_server_advanced_ops.py b/tempest/scenario/test_server_advanced_ops.py
index c0eb6e7..5a1dc04 100644
--- a/tempest/scenario/test_server_advanced_ops.py
+++ b/tempest/scenario/test_server_advanced_ops.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest import config
from tempest.openstack.common import log as logging
from tempest.scenario import manager
@@ -37,16 +39,12 @@
cls.set_network_resources()
super(TestServerAdvancedOps, cls).setUpClass()
- if not CONF.compute_feature_enabled.resize:
- msg = "Skipping test - resize not available on this host"
- raise cls.skipException(msg)
-
- resize_flavor = CONF.compute.flavor_ref_alt
-
- if resize_flavor == CONF.compute.flavor_ref:
+ if CONF.compute.flavor_ref_alt == CONF.compute.flavor_ref:
msg = "Skipping test - flavor_ref and flavor_ref_alt are identical"
raise cls.skipException(msg)
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize is not available.')
@test.services('compute')
def test_resize_server_confirm(self):
# We create an instance for use in this test
@@ -65,6 +63,8 @@
self.status_timeout(
self.compute_client.servers, instance_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.suspend,
+ 'Suspend is not available.')
@test.services('compute')
def test_server_sequence_suspend_resume(self):
# We create an instance for use in this test
diff --git a/tempest/scenario/test_server_basic_ops.py b/tempest/scenario/test_server_basic_ops.py
index d369f12..13e00a5 100644
--- a/tempest/scenario/test_server_basic_ops.py
+++ b/tempest/scenario/test_server_basic_ops.py
@@ -14,19 +14,17 @@
# under the License.
from tempest.common.utils import data_utils
-from tempest.common.utils import test_utils
from tempest import config
from tempest.openstack.common import log as logging
from tempest.scenario import manager
+from tempest.scenario import utils as test_utils
from tempest import test
-import testscenarios
-
CONF = config.CONF
LOG = logging.getLogger(__name__)
-load_tests = testscenarios.load_tests_apply_scenarios
+load_tests = test_utils.load_tests_input_scenario_utils
class TestServerBasicOps(manager.OfficialClientTest):
@@ -43,13 +41,6 @@
* Terminate the instance
"""
- scenario_utils = test_utils.InputScenarioUtils()
- scenario_flavor = scenario_utils.scenario_flavors
- scenario_image = scenario_utils.scenario_images
-
- scenarios = testscenarios.multiply_scenarios(scenario_image,
- scenario_flavor)
-
def setUp(self):
super(TestServerBasicOps, self).setUp()
# Setup image and flavor the test instance
@@ -99,42 +90,6 @@
create_kwargs=create_kwargs)
self.set_resource('instance', instance)
- def pause_server(self):
- instance = self.get_resource('instance')
- instance_id = instance.id
- LOG.debug("Pausing instance %s. Current status: %s",
- instance_id, instance.status)
- instance.pause()
- self.status_timeout(
- self.compute_client.servers, instance_id, 'PAUSED')
-
- def unpause_server(self):
- instance = self.get_resource('instance')
- instance_id = instance.id
- LOG.debug("Unpausing instance %s. Current status: %s",
- instance_id, instance.status)
- instance.unpause()
- self.status_timeout(
- self.compute_client.servers, instance_id, 'ACTIVE')
-
- def suspend_server(self):
- instance = self.get_resource('instance')
- instance_id = instance.id
- LOG.debug("Suspending instance %s. Current status: %s",
- instance_id, instance.status)
- instance.suspend()
- self.status_timeout(self.compute_client.servers,
- instance_id, 'SUSPENDED')
-
- def resume_server(self):
- instance = self.get_resource('instance')
- instance_id = instance.id
- LOG.debug("Resuming instance %s. Current status: %s",
- instance_id, instance.status)
- instance.resume()
- self.status_timeout(
- self.compute_client.servers, instance_id, 'ACTIVE')
-
def terminate_instance(self):
instance = self.get_resource('instance')
instance.delete()
@@ -149,10 +104,11 @@
instance.add_floating_ip(floating_ip)
# Check ssh
try:
- self.get_remote_client(
+ linux_client = self.get_remote_client(
server_or_ip=floating_ip.ip,
username=self.image_utils.ssh_user(self.image_ref),
- private_key=self.keypair.private)
+ private_key=self.keypair.private_key)
+ linux_client.validate_authentication()
except Exception:
LOG.exception('ssh to server failed')
self._log_console_output()
@@ -163,9 +119,5 @@
self.add_keypair()
self.create_security_group()
self.boot_instance()
- self.pause_server()
- self.unpause_server()
- self.suspend_server()
- self.resume_server()
self.verify_ssh()
self.terminate_instance()
diff --git a/tempest/common/utils/test_utils.py b/tempest/scenario/utils.py
similarity index 85%
rename from tempest/common/utils/test_utils.py
rename to tempest/scenario/utils.py
index cc0d831..4c7b6d7 100644
--- a/tempest/common/utils/test_utils.py
+++ b/tempest/scenario/utils.py
@@ -12,15 +12,19 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest import clients
-from tempest.common.utils import misc
-from tempest import config
import json
import re
import string
import unicodedata
+import testscenarios
+import testtools
+
+from tempest import clients
+from tempest.common.utils import misc
+from tempest import config
+
CONF = config.CONF
@@ -79,7 +83,7 @@
class TestInputScenario(manager.OfficialClientTest):
- scenario_utils = test_utils.InputScenarioUtils()
+ scenario_utils = utils.InputScenarioUtils()
scenario_flavor = scenario_utils.scenario_flavors
scenario_image = scenario_utils.scenario_images
scenarios = testscenarios.multiply_scenarios(scenario_image,
@@ -134,3 +138,22 @@
for f in flavors if re.search(self.flavor_pattern, str(f.name))
]
return self._scenario_flavors
+
+
+def load_tests_input_scenario_utils(*args):
+ """
+ Wrapper for testscenarios to set the scenarios to avoid running a getattr
+ on the CONF object at import.
+ """
+ if getattr(args[0], 'suiteClass', None) is not None:
+ loader, standard_tests, pattern = args
+ else:
+ standard_tests, module, loader = args
+ scenario_utils = InputScenarioUtils()
+ scenario_flavor = scenario_utils.scenario_flavors
+ scenario_image = scenario_utils.scenario_images
+ for test in testtools.iterate_tests(standard_tests):
+ setattr(test, 'scenarios', testscenarios.multiply_scenarios(
+ scenario_image,
+ scenario_flavor))
+ return testscenarios.load_tests_apply_scenarios(*args)
diff --git a/tempest/services/compute/json/agents_client.py b/tempest/services/compute/json/agents_client.py
new file mode 100644
index 0000000..19821e7
--- /dev/null
+++ b/tempest/services/compute/json/agents_client.py
@@ -0,0 +1,58 @@
+# Copyright 2014 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
+import urllib
+
+from tempest.api_schema.compute.v2 import agents as schema
+from tempest.common import rest_client
+from tempest import config
+
+CONF = config.CONF
+
+
+class AgentsClientJSON(rest_client.RestClient):
+ """
+ Tests Agents API
+ """
+
+ def __init__(self, auth_provider):
+ super(AgentsClientJSON, self).__init__(auth_provider)
+ self.service = CONF.compute.catalog_type
+
+ def list_agents(self, params=None):
+ """List all agent builds."""
+ url = 'os-agents'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ return resp, json.loads(body).get('agents')
+
+ def create_agent(self, **kwargs):
+ """Create an agent build."""
+ post_body = json.dumps({'agent': kwargs})
+ resp, body = self.post('os-agents', post_body)
+ return resp, self._parse_resp(body)
+
+ def delete_agent(self, agent_id):
+ """Delete an existing agent build."""
+ resp, body = self.delete("os-agents/%s" % str(agent_id))
+ self.validate_response(schema.delete_agent, resp, body)
+ return resp, body
+
+ def update_agent(self, agent_id, **kwargs):
+ """Update an agent build."""
+ put_body = json.dumps({'para': kwargs})
+ resp, body = self.put('os-agents/%s' % str(agent_id), put_body)
+ return resp, self._parse_resp(body)
diff --git a/tempest/services/compute/json/aggregates_client.py b/tempest/services/compute/json/aggregates_client.py
index ccb85c4..fe67102 100644
--- a/tempest/services/compute/json/aggregates_client.py
+++ b/tempest/services/compute/json/aggregates_client.py
@@ -40,6 +40,7 @@
"""Get details of the given aggregate."""
resp, body = self.get("os-aggregates/%s" % str(aggregate_id))
body = json.loads(body)
+ self.validate_response(schema.get_aggregate, resp, body)
return resp, body['aggregate']
def create_aggregate(self, **kwargs):
@@ -104,4 +105,5 @@
resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
post_body)
body = json.loads(body)
+ self.validate_response(schema.aggregate_set_metadata, resp, body)
return resp, body['aggregate']
diff --git a/tempest/services/compute/json/flavors_client.py b/tempest/services/compute/json/flavors_client.py
index bc4a64f..0206b82 100644
--- a/tempest/services/compute/json/flavors_client.py
+++ b/tempest/services/compute/json/flavors_client.py
@@ -18,6 +18,7 @@
from tempest.api_schema.compute import flavors as common_schema
from tempest.api_schema.compute import flavors_access as schema_access
+from tempest.api_schema.compute.v2 import flavors as v2schema
from tempest.common import rest_client
from tempest import config
@@ -47,6 +48,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(v2schema.list_flavors_details, resp, body)
return resp, body['flavors']
def get_flavor_details(self, flavor_id):
@@ -128,7 +130,8 @@
"""Gets flavor access information given the flavor id."""
resp, body = self.get('flavors/%s/os-flavor-access' % flavor_id)
body = json.loads(body)
- self.validate_response(schema_access.list_flavor_access, resp, body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
def add_flavor_access(self, flavor_id, tenant_id):
@@ -141,6 +144,8 @@
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
def remove_flavor_access(self, flavor_id, tenant_id):
@@ -153,4 +158,6 @@
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
diff --git a/tempest/services/compute/json/hosts_client.py b/tempest/services/compute/json/hosts_client.py
index a000e56..e148572 100644
--- a/tempest/services/compute/json/hosts_client.py
+++ b/tempest/services/compute/json/hosts_client.py
@@ -76,6 +76,7 @@
resp, body = self.get("os-hosts/%s/shutdown" % str(hostname))
body = json.loads(body)
+ self.validate_response(v2_schema.shutdown_host, resp, body)
return resp, body['host']
def reboot_host(self, hostname):
@@ -83,4 +84,5 @@
resp, body = self.get("os-hosts/%s/reboot" % str(hostname))
body = json.loads(body)
+ self.validate_response(v2_schema.reboot_host, resp, body)
return resp, body['host']
diff --git a/tempest/services/compute/json/instance_usage_audit_log_client.py b/tempest/services/compute/json/instance_usage_audit_log_client.py
index 4088be9..4700ca7 100644
--- a/tempest/services/compute/json/instance_usage_audit_log_client.py
+++ b/tempest/services/compute/json/instance_usage_audit_log_client.py
@@ -34,6 +34,8 @@
url = 'os-instance_usage_audit_log'
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(schema.list_instance_usage_audit_log,
+ resp, body)
return resp, body["instance_usage_audit_logs"]
def get_instance_usage_audit_log(self, time_before):
diff --git a/tempest/services/compute/json/migrations_client.py b/tempest/services/compute/json/migrations_client.py
new file mode 100644
index 0000000..a13349e
--- /dev/null
+++ b/tempest/services/compute/json/migrations_client.py
@@ -0,0 +1,39 @@
+# Copyright 2014 NEC Corporation.
+#
+# 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 import rest_client
+from tempest import config
+
+CONF = config.CONF
+
+
+class MigrationsClientJSON(rest_client.RestClient):
+
+ def __init__(self, auth_provider):
+ super(MigrationsClientJSON, self).__init__(auth_provider)
+ self.service = CONF.compute.catalog_type
+
+ def list_migrations(self, params=None):
+ """Lists all migrations."""
+
+ url = 'os-migrations'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['migrations']
diff --git a/tempest/services/compute/json/quotas_client.py b/tempest/services/compute/json/quotas_client.py
index 9346183..9bddf2c 100644
--- a/tempest/services/compute/json/quotas_client.py
+++ b/tempest/services/compute/json/quotas_client.py
@@ -48,8 +48,8 @@
self.validate_response(schema.quota_set, resp, body)
return resp, body['quota_set']
- def update_quota_set(self, tenant_id, force=None,
- injected_file_content_bytes=None,
+ def update_quota_set(self, tenant_id, user_id=None,
+ force=None, injected_file_content_bytes=None,
metadata_items=None, ram=None, floating_ips=None,
fixed_ips=None, key_pairs=None, instances=None,
security_group_rules=None, injected_files=None,
@@ -101,11 +101,19 @@
post_body['security_groups'] = security_groups
post_body = json.dumps({'quota_set': post_body})
- resp, body = self.put('os-quota-sets/%s' % str(tenant_id), post_body)
+
+ if user_id:
+ resp, body = self.put('os-quota-sets/%s?user_id=%s' %
+ (str(tenant_id), str(user_id)), post_body)
+ else:
+ resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
+ post_body)
body = json.loads(body)
return resp, body['quota_set']
def delete_quota_set(self, tenant_id):
"""Delete the tenant's quota set."""
- return self.delete('os-quota-sets/%s' % str(tenant_id))
+ resp, body = self.delete('os-quota-sets/%s' % str(tenant_id))
+ self.validate_response(schema.delete_quota, resp, body)
+ return resp, body
diff --git a/tempest/services/compute/json/servers_client.py b/tempest/services/compute/json/servers_client.py
index 2d23b03..cf1dad9 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -198,12 +198,21 @@
body = json.loads(body)
return resp, body
- def action(self, server_id, action_name, response_key, **kwargs):
+ def action(self, server_id, action_name, response_key,
+ schema=None, **kwargs):
post_body = json.dumps({action_name: kwargs})
resp, body = self.post('servers/%s/action' % str(server_id),
post_body)
if response_key is not None:
- body = json.loads(body)[response_key]
+ body = json.loads(body)
+ # Check for Schema as 'None' because if we donot have any server
+ # action schema implemented yet then they can pass 'None' to skip
+ # the validation.Once all server action has their schema
+ # implemented then, this check can be removed if every actions are
+ # supposed to validate their response.
+ if schema is not None:
+ self.validate_response(schema, resp, body)
+ body = body[response_key]
return resp, body
def create_backup(self, server_id, backup_type, rotation, name):
@@ -262,10 +271,6 @@
"""Reverts a server back to its original flavor."""
return self.action(server_id, 'revertResize', None, **kwargs)
- def create_image(self, server_id, name):
- """Creates an image of the given server."""
- return self.action(server_id, 'createImage', None, name=name)
-
def list_server_metadata(self, server_id):
resp, body = self.get("servers/%s/metadata" % str(server_id))
body = json.loads(body)
@@ -405,7 +410,9 @@
"""
resp, body = self.get('/'.join(['servers', server_id,
'os-virtual-interfaces']))
- return resp, json.loads(body)
+ body = json.loads(body)
+ self.validate_response(schema.list_virtual_interfaces, resp, body)
+ return resp, body
def rescue_server(self, server_id, **kwargs):
"""Rescue the provided server."""
@@ -453,4 +460,5 @@
def get_vnc_console(self, server_id, console_type):
"""Get URL of VNC console."""
return self.action(server_id, "os-getVNCConsole",
- "console", type=console_type)
+ "console", common_schema.get_vnc_console,
+ type=console_type)
diff --git a/tempest/services/compute/json/volumes_extensions_client.py b/tempest/services/compute/json/volumes_extensions_client.py
index 17468eb..d1014af 100644
--- a/tempest/services/compute/json/volumes_extensions_client.py
+++ b/tempest/services/compute/json/volumes_extensions_client.py
@@ -61,7 +61,7 @@
url = "os-volumes/%s" % str(volume_id)
resp, body = self.get(url)
body = json.loads(body)
- self.validate_response(schema.get_volume, resp, body)
+ self.validate_response(schema.create_get_volume, resp, body)
return resp, body['volume']
def create_volume(self, size, **kwargs):
@@ -81,11 +81,14 @@
post_body = json.dumps({'volume': post_body})
resp, body = self.post('os-volumes', post_body)
body = json.loads(body)
+ self.validate_response(schema.create_get_volume, resp, body)
return resp, body['volume']
def delete_volume(self, volume_id):
"""Deletes the Specified Volume."""
- return self.delete("os-volumes/%s" % str(volume_id))
+ resp, body = self.delete("os-volumes/%s" % str(volume_id))
+ self.validate_response(schema.delete_volume, resp, body)
+ return resp, body
def wait_for_volume_status(self, volume_id, status):
"""Waits for a Volume to reach a given status."""
diff --git a/tempest/services/compute/v3/json/agents_client.py b/tempest/services/compute/v3/json/agents_client.py
index 6893af2..e1c286c 100644
--- a/tempest/services/compute/v3/json/agents_client.py
+++ b/tempest/services/compute/v3/json/agents_client.py
@@ -15,6 +15,7 @@
import json
import urllib
+from tempest.api_schema.compute.v3 import agents as schema
from tempest.common import rest_client
from tempest import config
@@ -43,7 +44,9 @@
def delete_agent(self, agent_id):
"""Delete an existing agent build."""
- return self.delete('os-agents/%s' % str(agent_id))
+ resp, body = self.delete("os-agents/%s" % str(agent_id))
+ self.validate_response(schema.delete_agent, resp, body)
+ return resp, body
def update_agent(self, agent_id, **kwargs):
"""Update an agent build."""
diff --git a/tempest/services/compute/v3/json/aggregates_client.py b/tempest/services/compute/v3/json/aggregates_client.py
index 7f73622..8d7440e 100644
--- a/tempest/services/compute/v3/json/aggregates_client.py
+++ b/tempest/services/compute/v3/json/aggregates_client.py
@@ -40,6 +40,7 @@
"""Get details of the given aggregate."""
resp, body = self.get("os-aggregates/%s" % str(aggregate_id))
body = json.loads(body)
+ self.validate_response(schema.get_aggregate, resp, body)
return resp, body['aggregate']
def create_aggregate(self, **kwargs):
@@ -104,4 +105,5 @@
resp, body = self.post('os-aggregates/%s/action' % aggregate_id,
post_body)
body = json.loads(body)
+ self.validate_response(schema.aggregate_set_metadata, resp, body)
return resp, body['aggregate']
diff --git a/tempest/services/compute/v3/json/flavors_client.py b/tempest/services/compute/v3/json/flavors_client.py
index 3fdb3ca..189fe3f 100644
--- a/tempest/services/compute/v3/json/flavors_client.py
+++ b/tempest/services/compute/v3/json/flavors_client.py
@@ -18,6 +18,7 @@
from tempest.api_schema.compute import flavors as common_schema
from tempest.api_schema.compute import flavors_access as schema_access
+from tempest.api_schema.compute.v3 import flavors as v3schema
from tempest.common import rest_client
from tempest import config
@@ -47,6 +48,7 @@
resp, body = self.get(url)
body = json.loads(body)
+ self.validate_response(v3schema.list_flavors_details, resp, body)
return resp, body['flavors']
def get_flavor_details(self, flavor_id):
@@ -128,7 +130,8 @@
"""Gets flavor access information given the flavor id."""
resp, body = self.get('flavors/%s/flavor-access' % flavor_id)
body = json.loads(body)
- self.validate_response(schema_access.list_flavor_access, resp, body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
def add_flavor_access(self, flavor_id, tenant_id):
@@ -141,6 +144,8 @@
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
def remove_flavor_access(self, flavor_id, tenant_id):
@@ -153,4 +158,6 @@
post_body = json.dumps(post_body)
resp, body = self.post('flavors/%s/action' % flavor_id, post_body)
body = json.loads(body)
+ self.validate_response(schema_access.add_remove_list_flavor_access,
+ resp, body)
return resp, body['flavor_access']
diff --git a/tempest/services/compute/v3/json/hosts_client.py b/tempest/services/compute/v3/json/hosts_client.py
index 558d4f7..24d43d0 100644
--- a/tempest/services/compute/v3/json/hosts_client.py
+++ b/tempest/services/compute/v3/json/hosts_client.py
@@ -76,6 +76,7 @@
resp, body = self.get("os-hosts/%s/shutdown" % str(hostname))
body = json.loads(body)
+ self.validate_response(v3_schema.shutdown_host, resp, body)
return resp, body['host']
def reboot_host(self, hostname):
@@ -83,4 +84,5 @@
resp, body = self.get("os-hosts/%s/reboot" % str(hostname))
body = json.loads(body)
+ self.validate_response(v3_schema.reboot_host, resp, body)
return resp, body['host']
diff --git a/tempest/services/compute/v3/json/quotas_client.py b/tempest/services/compute/v3/json/quotas_client.py
index 870c22e..37a8906 100644
--- a/tempest/services/compute/v3/json/quotas_client.py
+++ b/tempest/services/compute/v3/json/quotas_client.py
@@ -57,7 +57,7 @@
self.validate_response(schema.quota_set, resp, body)
return resp, body['quota_set']
- def update_quota_set(self, tenant_id, force=None,
+ def update_quota_set(self, tenant_id, user_id=None, force=None,
metadata_items=None, ram=None, floating_ips=None,
fixed_ips=None, key_pairs=None, instances=None,
security_group_rules=None, cores=None,
@@ -98,7 +98,13 @@
post_body['security_groups'] = security_groups
post_body = json.dumps({'quota_set': post_body})
- resp, body = self.put('os-quota-sets/%s' % str(tenant_id), post_body)
+
+ if user_id:
+ resp, body = self.put('os-quota-sets/%s?user_id=%s' %
+ (str(tenant_id), str(user_id)), post_body)
+ else:
+ resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
+ post_body)
body = json.loads(body)
self.validate_response(schema.quota_set, resp, body)
@@ -106,4 +112,6 @@
def delete_quota_set(self, tenant_id):
"""Delete the tenant's quota set."""
- return self.delete('os-quota-sets/%s' % str(tenant_id))
+ resp, body = self.delete('os-quota-sets/%s' % str(tenant_id))
+ self.validate_response(schema.delete_quota, resp, body)
+ return resp, body
diff --git a/tempest/services/compute/v3/json/servers_client.py b/tempest/services/compute/v3/json/servers_client.py
index 6a0d9b2..caeb315 100644
--- a/tempest/services/compute/v3/json/servers_client.py
+++ b/tempest/services/compute/v3/json/servers_client.py
@@ -410,19 +410,19 @@
str(server_id))
return resp, json.loads(body)
- def list_instance_actions(self, server_id):
+ def list_server_actions(self, server_id):
"""List the provided server action."""
- resp, body = self.get("servers/%s/os-instance-actions" %
+ resp, body = self.get("servers/%s/os-server-actions" %
str(server_id))
body = json.loads(body)
- return resp, body['instance_actions']
+ return resp, body['server_actions']
- def get_instance_action(self, server_id, request_id):
+ def get_server_action(self, server_id, request_id):
"""Returns the action details of the provided server."""
- resp, body = self.get("servers/%s/os-instance-actions/%s" %
+ resp, body = self.get("servers/%s/os-server-actions/%s" %
(str(server_id), str(request_id)))
body = json.loads(body)
- return resp, body['instance_action']
+ return resp, body['server_action']
def force_delete_server(self, server_id, **kwargs):
"""Force delete a server."""
@@ -442,6 +442,7 @@
resp, body = self.post('servers/%s/action' % str(server_id),
post_body)
body = json.loads(body)
+ self.validate_response(common_schema.get_vnc_console, resp, body)
return resp, body['console']
def reset_network(self, server_id, **kwargs):
diff --git a/tempest/services/compute/xml/quotas_client.py b/tempest/services/compute/xml/quotas_client.py
index 8a521ab..5502fcc 100644
--- a/tempest/services/compute/xml/quotas_client.py
+++ b/tempest/services/compute/xml/quotas_client.py
@@ -61,8 +61,8 @@
body = self._format_quota(body)
return resp, body
- def update_quota_set(self, tenant_id, force=None,
- injected_file_content_bytes=None,
+ def update_quota_set(self, tenant_id, user_id=None,
+ force=None, injected_file_content_bytes=None,
metadata_items=None, ram=None, floating_ips=None,
fixed_ips=None, key_pairs=None, instances=None,
security_group_rules=None, injected_files=None,
@@ -115,8 +115,14 @@
if security_groups is not None:
post_body.add_attr('security_groups', security_groups)
- resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
- str(xml_utils.Document(post_body)))
+ if user_id:
+ resp, body = self.put('os-quota-sets/%s?user_id=%s' %
+ (str(tenant_id), str(user_id)),
+ str(xml_utils.Document(post_body)))
+ else:
+ resp, body = self.put('os-quota-sets/%s' % str(tenant_id),
+ str(xml_utils.Document(post_body)))
+
body = xml_utils.xml_to_json(etree.fromstring(body))
body = self._format_quota(body)
return resp, body
diff --git a/tempest/services/identity/json/identity_client.py b/tempest/services/identity/json/identity_client.py
index fe176bf..55239f7 100644
--- a/tempest/services/identity/json/identity_client.py
+++ b/tempest/services/identity/json/identity_client.py
@@ -275,13 +275,20 @@
return resp, body['access']
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
if headers is None:
# Always accept 'json', for TokenClientXML too.
# Because XML response is not easily
# converted to the corresponding JSON one
headers = self.get_headers(accept_type="json")
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers(accept_type="json"))
+ except (ValueError, TypeError):
+ headers = self.get_headers(accept_type="json")
+
resp, resp_body = self.http_obj.request(url, method,
headers=headers, body=body)
self._log_request(method, url, resp)
diff --git a/tempest/services/identity/v3/json/identity_client.py b/tempest/services/identity/v3/json/identity_client.py
index 4b530f1..6829333 100644
--- a/tempest/services/identity/v3/json/identity_client.py
+++ b/tempest/services/identity/v3/json/identity_client.py
@@ -163,6 +163,12 @@
body = json.loads(body)
return resp, body['role']
+ def list_roles(self):
+ """Get the list of Roles."""
+ resp, body = self.get("roles")
+ body = json.loads(body)
+ return resp, body['roles']
+
def update_role(self, name, role_id):
"""Create a Role."""
post_body = {
@@ -515,13 +521,20 @@
resp, body = self.post(self.auth_url, body=body)
return resp, body
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
if headers is None:
# Always accept 'json', for xml token client too.
# Because XML response is not easily
# converted to the corresponding JSON one
headers = self.get_headers(accept_type="json")
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers(accept_type="json"))
+ except (ValueError, TypeError):
+ headers = self.get_headers(accept_type="json")
+
resp, resp_body = self.http_obj.request(url, method,
headers=headers, body=body)
self._log_request(method, url, resp)
diff --git a/tempest/services/identity/v3/xml/endpoints_client.py b/tempest/services/identity/v3/xml/endpoints_client.py
index 93dc3dc..6490e34 100644
--- a/tempest/services/identity/v3/xml/endpoints_client.py
+++ b/tempest/services/identity/v3/xml/endpoints_client.py
@@ -46,12 +46,19 @@
json = common.xml_to_json(body)
return json
- def request(self, method, url, headers=None, body=None, wait=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, wait=None):
"""Overriding the existing HTTP request in super class RestClient."""
+ if extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = self.get_headers()
dscv = CONF.identity.disable_ssl_certificate_validation
self.http_obj = http.ClosingHttp(
disable_ssl_certificate_validation=dscv)
return super(EndPointClientXML, self).request(method, url,
+ extra_headers,
headers=headers,
body=body)
diff --git a/tempest/services/identity/v3/xml/identity_client.py b/tempest/services/identity/v3/xml/identity_client.py
index c49f361..35295d7 100644
--- a/tempest/services/identity/v3/xml/identity_client.py
+++ b/tempest/services/identity/v3/xml/identity_client.py
@@ -217,6 +217,12 @@
body = self._parse_body(etree.fromstring(body))
return resp, body
+ def list_roles(self):
+ """Get the list of Roles."""
+ resp, body = self.get("roles")
+ body = self._parse_roles(etree.fromstring(body))
+ return resp, body
+
def update_role(self, name, role_id):
"""Updates a Role."""
post_body = common.Element("role",
@@ -516,13 +522,19 @@
resp, body = self.post(self.auth_url, body=str(common.Document(auth)))
return resp, body
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
if headers is None:
# Always accept 'json', for xml token client too.
# Because XML response is not easily
# converted to the corresponding JSON one
headers = self.get_headers(accept_type="json")
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers(accept_type="json"))
+ except (ValueError, TypeError):
+ headers = self.get_headers(accept_type="json")
resp, resp_body = self.http_obj.request(url, method,
headers=headers, body=body)
self._log_request(method, url, resp)
diff --git a/tempest/services/identity/v3/xml/policy_client.py b/tempest/services/identity/v3/xml/policy_client.py
index e903089..73d831b 100644
--- a/tempest/services/identity/v3/xml/policy_client.py
+++ b/tempest/services/identity/v3/xml/policy_client.py
@@ -46,12 +46,19 @@
json = common.xml_to_json(body)
return json
- def request(self, method, url, headers=None, body=None, wait=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None, wait=None):
"""Overriding the existing HTTP request in super class RestClient."""
+ if extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = self.get_headers()
dscv = CONF.identity.disable_ssl_certificate_validation
self.http_obj = http.ClosingHttp(
disable_ssl_certificate_validation=dscv)
return super(PolicyClientXML, self).request(method, url,
+ extra_headers,
headers=headers,
body=body)
diff --git a/tempest/services/network/xml/network_client.py b/tempest/services/network/xml/network_client.py
index 0945b09..50a1954 100644
--- a/tempest/services/network/xml/network_client.py
+++ b/tempest/services/network/xml/network_client.py
@@ -24,7 +24,7 @@
# list of plurals used for xml serialization
PLURALS = ['dns_nameservers', 'host_routes', 'allocation_pools',
'fixed_ips', 'extensions', 'extra_dhcp_opts', 'pools',
- 'health_monitors', 'vips']
+ 'health_monitors', 'vips', 'members']
def get_rest_client(self, auth_provider):
rc = rest_client.RestClient(auth_provider)
@@ -257,6 +257,38 @@
body = _root_tag_fetcher_and_xml_to_json_parse(body)
return resp, body
+ def create_vpnservice(self, subnet_id, router_id, **kwargs):
+ uri = '%s/vpn/vpnservices' % (self.uri_prefix)
+ vpnservice = common.Element("vpnservice")
+ p1 = common.Element("subnet_id", subnet_id)
+ p2 = common.Element("router_id", router_id)
+ vpnservice.append(p1)
+ vpnservice.append(p2)
+ common.deep_dict_to_xml(vpnservice, kwargs)
+ resp, body = self.post(uri, str(common.Document(vpnservice)))
+ body = _root_tag_fetcher_and_xml_to_json_parse(body)
+ return resp, body
+
+ def create_ikepolicy(self, name, **kwargs):
+ uri = '%s/vpn/ikepolicies' % (self.uri_prefix)
+ ikepolicy = common.Element("ikepolicy")
+ p1 = common.Element("name", name)
+ ikepolicy.append(p1)
+ common.deep_dict_to_xml(ikepolicy, kwargs)
+ resp, body = self.post(uri, str(common.Document(ikepolicy)))
+ body = _root_tag_fetcher_and_xml_to_json_parse(body)
+ return resp, body
+
+ def create_ipsecpolicy(self, name, **kwargs):
+ uri = '%s/vpn/ipsecpolicies' % (self.uri_prefix)
+ ipsecpolicy = common.Element("ipsecpolicy")
+ p1 = common.Element("name", name)
+ ipsecpolicy.append(p1)
+ common.deep_dict_to_xml(ipsecpolicy, kwargs)
+ resp, body = self.post(uri, str(common.Document(ipsecpolicy)))
+ body = _root_tag_fetcher_and_xml_to_json_parse(body)
+ return resp, body
+
def _root_tag_fetcher_and_xml_to_json_parse(xml_returned_body):
body = ET.fromstring(xml_returned_body)
diff --git a/tempest/services/object_storage/account_client.py b/tempest/services/object_storage/account_client.py
index 6e7910e..a0506f2 100644
--- a/tempest/services/object_storage/account_client.py
+++ b/tempest/services/object_storage/account_client.py
@@ -162,11 +162,17 @@
self.service = CONF.object_storage.catalog_type
self.format = 'json'
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
self.http_obj = http.ClosingHttp()
if headers is None:
headers = {}
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = {}
# Authorize the request
req_url, req_headers, req_body = self.auth_provider.auth_request(
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index 49f7f49..53a3325 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -29,12 +29,16 @@
self.service = CONF.object_storage.catalog_type
- def create_object(self, container, object_name, data, params=None):
+ def create_object(self, container, object_name, data,
+ params=None, metadata=None):
"""Create storage object."""
headers = self.get_headers()
if not data:
headers['content-length'] = '0'
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
url = "%s/%s" % (str(container), str(object_name))
if params:
url += '?%s' % urllib.urlencode(params)
@@ -146,13 +150,19 @@
self.service = CONF.object_storage.catalog_type
self.format = 'json'
- def request(self, method, url, headers=None, body=None):
+ def request(self, method, url, extra_headers=False, headers=None,
+ body=None):
"""A simple HTTP request interface."""
dscv = CONF.identity.disable_ssl_certificate_validation
self.http_obj = http.ClosingHttp(
disable_ssl_certificate_validation=dscv)
if headers is None:
headers = {}
+ elif extra_headers:
+ try:
+ headers.update(self.get_headers())
+ except (ValueError, TypeError):
+ headers = {}
# Authorize the request
req_url, req_headers, req_body = self.auth_provider.auth_request(
diff --git a/tempest/services/orchestration/json/orchestration_client.py b/tempest/services/orchestration/json/orchestration_client.py
index 113003c..2311bdd 100644
--- a/tempest/services/orchestration/json/orchestration_client.py
+++ b/tempest/services/orchestration/json/orchestration_client.py
@@ -154,7 +154,8 @@
if resource_status == status:
return
if fail_regexp.search(resource_status):
- raise exceptions.StackBuildErrorException(
+ raise exceptions.StackResourceBuildErrorException(
+ resource_name=resource_name,
stack_identifier=stack_identifier,
resource_status=resource_status,
resource_status_reason=body['resource_status_reason'])
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index e4d2e8d..b55a037 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -67,10 +67,10 @@
body = json.loads(body)
return resp, body['volume']
- def create_volume(self, size, **kwargs):
+ def create_volume(self, size=None, **kwargs):
"""
Creates a new Volume.
- size(Required): Size of volume in GB.
+ size: Size of volume in GB.
Following optional keyword arguments are accepted:
display_name: Optional Volume Name.
metadata: A dictionary of values to be used as metadata.
@@ -78,6 +78,10 @@
snapshot_id: When specified the volume is created from this snapshot
imageRef: When specified the volume is created from this image
"""
+ # for bug #1293885:
+ # If no size specified, read volume size from CONF
+ if size is None:
+ size = CONF.volume.volume_size
post_body = {'size': size}
post_body.update(kwargs)
post_body = json.dumps({'volume': post_body})
diff --git a/tempest/services/volume/v2/json/volumes_client.py b/tempest/services/volume/v2/json/volumes_client.py
index 5bfa75f..df20a2a 100644
--- a/tempest/services/volume/v2/json/volumes_client.py
+++ b/tempest/services/volume/v2/json/volumes_client.py
@@ -68,10 +68,10 @@
body = json.loads(body)
return resp, body['volume']
- def create_volume(self, size, **kwargs):
+ def create_volume(self, size=None, **kwargs):
"""
Creates a new Volume.
- size(Required): Size of volume in GB.
+ size: Size of volume in GB.
Following optional keyword arguments are accepted:
name: Optional Volume Name.
metadata: A dictionary of values to be used as metadata.
@@ -79,6 +79,10 @@
snapshot_id: When specified the volume is created from this snapshot
imageRef: When specified the volume is created from this image
"""
+ # for bug #1293885:
+ # If no size specified, read volume size from CONF
+ if size is None:
+ size = CONF.volume.volume_size
post_body = {'size': size}
post_body.update(kwargs)
post_body = json.dumps({'volume': post_body})
diff --git a/tempest/services/volume/v2/xml/volumes_client.py b/tempest/services/volume/v2/xml/volumes_client.py
index e735a65..1fdaf19 100644
--- a/tempest/services/volume/v2/xml/volumes_client.py
+++ b/tempest/services/volume/v2/xml/volumes_client.py
@@ -117,10 +117,10 @@
body = self._check_if_bootable(body)
return resp, body
- def create_volume(self, size, **kwargs):
+ def create_volume(self, size=None, **kwargs):
"""Creates a new Volume.
- :param size: Size of volume in GB. (Required)
+ :param size: Size of volume in GB.
:param name: Optional Volume Name.
:param metadata: An optional dictionary of values for metadata.
:param volume_type: Optional Name of volume_type for the volume
@@ -129,6 +129,10 @@
:param imageRef: When specified the volume is created from this
image
"""
+ # for bug #1293885:
+ # If no size specified, read volume size from CONF
+ if size is None:
+ size = CONF.volume.volume_size
# NOTE(afazekas): it should use a volume namespace
volume = common.Element("volume", xmlns=common.XMLNS_11, size=size)
diff --git a/tempest/services/volume/xml/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
index 6866dad..65bc321 100644
--- a/tempest/services/volume/xml/volumes_client.py
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -118,10 +118,10 @@
body = self._check_if_bootable(body)
return resp, body
- def create_volume(self, size, **kwargs):
+ def create_volume(self, size=None, **kwargs):
"""Creates a new Volume.
- :param size: Size of volume in GB. (Required)
+ :param size: Size of volume in GB.
:param display_name: Optional Volume Name.
:param metadata: An optional dictionary of values for metadata.
:param volume_type: Optional Name of volume_type for the volume
@@ -130,6 +130,10 @@
:param imageRef: When specified the volume is created from this
image
"""
+ # for bug #1293885:
+ # If no size specified, read volume size from CONF
+ if size is None:
+ size = CONF.volume.volume_size
# NOTE(afazekas): it should use a volume namespace
volume = common.Element("volume", xmlns=common.XMLNS_11, size=size)
diff --git a/tempest/test.py b/tempest/test.py
index e4019f9..8df405c 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -94,6 +94,7 @@
service_list = {
'compute': CONF.service_available.nova,
'image': CONF.service_available.glance,
+ 'baremetal': CONF.service_available.ironic,
'volume': CONF.service_available.cinder,
'orchestration': CONF.service_available.heat,
# NOTE(mtreinish) nova-network will provide networking functionality
diff --git a/tempest/tests/common/test_debug.py b/tempest/tests/common/test_debug.py
index cd9936c..8a880f2 100644
--- a/tempest/tests/common/test_debug.py
+++ b/tempest/tests/common/test_debug.py
@@ -53,15 +53,14 @@
self.useFixture(mockpatch.PatchObject(test.CONF.debug,
'enable', True))
- tables = ['filter', 'nat', 'mangle']
self.ip_ns_list_mock.return_value = [1, 2]
debug.log_ip_ns()
self.ip_addr_raw_mock.assert_called_with()
self.assertTrue(self.log_mock.info.called)
self.ip_route_raw_mock.assert_called_with()
- self.assertEqual(len(tables), self.iptables_raw_mock.call_count)
- for table in tables:
+ self.assertEqual(len(debug.TABLES), self.iptables_raw_mock.call_count)
+ for table in debug.TABLES:
self.assertIn(mock.call(table),
self.iptables_raw_mock.call_args_list)
@@ -76,10 +75,11 @@
self.assertIn(mock.call(ns),
self.ip_ns_route_mock.call_args_list)
- self.assertEqual(len(tables) * len(self.ip_ns_list_mock.return_value),
+ self.assertEqual(len(debug.TABLES) *
+ len(self.ip_ns_list_mock.return_value),
self.iptables_ns_mock.call_count)
for ns in self.ip_ns_list_mock.return_value:
- for table in tables:
+ for table in debug.TABLES:
self.assertIn(mock.call(ns, table),
self.iptables_ns_mock.call_args_list)
diff --git a/tempest/tests/test_commands.py b/tempest/tests/test_commands.py
new file mode 100644
index 0000000..bdb9269
--- /dev/null
+++ b/tempest/tests/test_commands.py
@@ -0,0 +1,87 @@
+# Copyright 2014 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 mock
+import subprocess
+
+from tempest.common import commands
+from tempest.tests import base
+
+
+class TestCommands(base.TestCase):
+
+ def setUp(self):
+ super(TestCommands, self).setUp()
+ self.subprocess_args = {'stdout': subprocess.PIPE,
+ 'stderr': subprocess.STDOUT}
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_addr_raw(self, mock):
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'a']
+ commands.ip_addr_raw()
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_route_raw(self, mock):
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'r']
+ commands.ip_route_raw()
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_ns_raw(self, mock):
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'list']
+ commands.ip_ns_raw()
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_iptables_raw(self, mock):
+ table = 'filter'
+ expected = ['/usr/bin/sudo', '-n', 'iptables', '-v', '-S', '-t',
+ '%s' % table]
+ commands.iptables_raw(table)
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_ns_list(self, mock):
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'list']
+ commands.ip_ns_list()
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_ns_addr(self, mock):
+ ns_list = commands.ip_ns_list()
+ for ns in ns_list:
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'exec', ns,
+ 'ip', 'a']
+ commands.ip_ns_addr(ns)
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_ip_ns_route(self, mock):
+ ns_list = commands.ip_ns_list()
+ for ns in ns_list:
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'exec', ns,
+ 'ip', 'r']
+ commands.ip_ns_route(ns)
+ mock.assert_called_once_with(expected, **self.subprocess_args)
+
+ @mock.patch('subprocess.Popen')
+ def test_iptables_ns(self, mock):
+ table = 'filter'
+ ns_list = commands.ip_ns_list()
+ for ns in ns_list:
+ expected = ['/usr/bin/sudo', '-n', 'ip', 'netns', 'exec', ns,
+ 'iptables', '-v', '-S', '-t', table]
+ commands.iptables_ns(ns, table)
+ mock.assert_called_once_with(expected, **self.subprocess_args)
diff --git a/tempest/tests/test_rest_client.py b/tempest/tests/test_rest_client.py
index cfbb37d..64ad3bc 100644
--- a/tempest/tests/test_rest_client.py
+++ b/tempest/tests/test_rest_client.py
@@ -139,6 +139,102 @@
self._verify_headers(resp)
+class TestRestClientUpdateHeaders(BaseRestClientTestClass):
+ def setUp(self):
+ self.fake_http = fake_http.fake_httplib2()
+ super(TestRestClientUpdateHeaders, self).setUp()
+ self.useFixture(mockpatch.PatchObject(self.rest_client,
+ '_error_checker'))
+ self.headers = {'X-Configuration-Session': 'session_id'}
+
+ def test_post_update_headers(self):
+ __, return_dict = self.rest_client.post(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_get_update_headers(self):
+ __, return_dict = self.rest_client.get(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_delete_update_headers(self):
+ __, return_dict = self.rest_client.delete(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_patch_update_headers(self):
+ __, return_dict = self.rest_client.patch(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_put_update_headers(self):
+ __, return_dict = self.rest_client.put(self.url, {},
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_head_update_headers(self):
+ self.useFixture(mockpatch.PatchObject(self.rest_client,
+ 'response_checker'))
+
+ __, return_dict = self.rest_client.head(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+ def test_copy_update_headers(self):
+ __, return_dict = self.rest_client.copy(self.url,
+ extra_headers=True,
+ headers=self.headers)
+
+ self.assertDictContainsSubset(
+ {'X-Configuration-Session': 'session_id',
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json'},
+ return_dict['headers']
+ )
+
+
class TestRestClientHeadersXML(TestRestClientHeadersJSON):
TYPE = "xml"
diff --git a/tools/check_logs.py b/tools/check_logs.py
index b5b1780..bc4eaca 100755
--- a/tools/check_logs.py
+++ b/tools/check_logs.py
@@ -46,7 +46,6 @@
'n-api',
'n-cpu',
'n-net',
- 'n-sch',
'q-agt',
'q-dhcp',
'q-lbaas',
diff --git a/tools/verify_tempest_config.py b/tools/verify_tempest_config.py
index aa92c0b..30785c4 100755
--- a/tools/verify_tempest_config.py
+++ b/tools/verify_tempest_config.py
@@ -16,6 +16,7 @@
import json
import sys
+import urlparse
import httplib2
@@ -39,19 +40,37 @@
not CONF.image_feature_enabled.api_v2))
-def verify_nova_api_versions(os):
- # Check nova api versions - only get base URL without PATH
- os.servers_client.skip_path = True
- # The nova base endpoint url includes the version but to get the versions
- # list the unversioned endpoint is needed
- v2_endpoint = os.servers_client.base_url
- v2_endpoint_parts = v2_endpoint.split('/')
- endpoint = v2_endpoint_parts[0] + '//' + v2_endpoint_parts[2]
+def _get_api_versions(os, service):
+ client_dict = {
+ 'nova': os.servers_client,
+ 'keystone': os.identity_client,
+ }
+ client_dict[service].skip_path()
+ endpoint_parts = urlparse.urlparse(client_dict[service])
+ endpoint = endpoint_parts.scheme + '//' + endpoint_parts.netloc
__, body = RAW_HTTP.request(endpoint, 'GET')
+ client_dict[service].reset_path()
body = json.loads(body)
- # Restore full base_url
- os.servers_client.skip_path = False
- versions = map(lambda x: x['id'], body['versions'])
+ if service == 'keystone':
+ versions = map(lambda x: x['id'], body['versions']['values'])
+ else:
+ versions = map(lambda x: x['id'], body['versions'])
+ return versions
+
+
+def verify_keystone_api_versions(os):
+ # Check keystone api versions
+ versions = _get_api_versions(os, 'keystone')
+ if CONF.identity_feature_enabled.api_v2 != ('v2.0' in versions):
+ print('Config option identity api_v2 should be change to %s' % (
+ not CONF.identity_feature_enabled.api_v2))
+ if CONF.identity_feature_enabled.api_v3 != ('v3.0' in versions):
+ print('Config option identity api_v3 should be change to %s' % (
+ not CONF.identity_feature_enabled.api_v3))
+
+
+def verify_nova_api_versions(os):
+ versions = _get_api_versions(os, 'nova')
if CONF.compute_feature_enabled.api_v3 != ('v3.0' in versions):
print('Config option compute api_v3 should be change to: %s' % (
not CONF.compute_feature_enabled.api_v3))
@@ -197,6 +216,7 @@
elif service not in services:
continue
results = verify_extensions(os, service, results)
+ verify_keystone_api_versions(os)
verify_glance_api_versions(os)
verify_nova_api_versions(os)
display_results(results)