Merge "ssh instance validation add options for Neutron"
diff --git a/tempest/api/baremetal/test_nodestates.py b/tempest/api/baremetal/test_nodestates.py
new file mode 100644
index 0000000..c658d7f
--- /dev/null
+++ b/tempest/api/baremetal/test_nodestates.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.
+
+from tempest.api.baremetal import base
+from tempest import test
+
+
+class TestNodeStates(base.BaseBaremetalTest):
+ """Tests for baremetal NodeStates."""
+
+ @classmethod
+ def setUpClass(self):
+ super(TestNodeStates, self).setUpClass()
+ chassis = self.create_chassis()['chassis']
+ self.node = self.create_node(chassis['uuid'])['node']
+
+ @test.attr(type='smoke')
+ def test_list_nodestates(self):
+ resp, nodestates = self.client.list_nodestates(self.node['uuid'])
+ self.assertEqual('200', resp['status'])
+ for key in nodestates:
+ self.assertEqual(nodestates[key], self.node[key])
diff --git a/tempest/api/compute/admin/test_aggregates.py b/tempest/api/compute/admin/test_aggregates.py
index fb249e5..c2376c9 100644
--- a/tempest/api/compute/admin/test_aggregates.py
+++ b/tempest/api/compute/admin/test_aggregates.py
@@ -16,6 +16,7 @@
from tempest.api.compute import base
from tempest.common import tempest_fixtures as fixtures
from tempest.common.utils import data_utils
+from tempest import exceptions
from tempest import test
@@ -39,11 +40,20 @@
filter(lambda y: y['service'] == 'compute', hosts_all))
cls.host = hosts[0]
+ def _try_delete_aggregate(self, aggregate_id):
+ # delete aggregate, if it exists
+ try:
+ self.client.delete_aggregate(aggregate_id)
+ # if aggregate not found, it depict it was deleted in the test
+ except exceptions.NotFound:
+ pass
+
@test.attr(type='gate')
def test_aggregate_create_delete(self):
# Create and delete an aggregate.
aggregate_name = data_utils.rand_name(self.aggregate_name_prefix)
resp, aggregate = self.client.create_aggregate(name=aggregate_name)
+ self.addCleanup(self._try_delete_aggregate, aggregate['id'])
self.assertEqual(200, resp.status)
self.assertEqual(aggregate_name, aggregate['name'])
self.assertIsNone(aggregate['availability_zone'])
@@ -59,6 +69,7 @@
az_name = data_utils.rand_name(self.az_name_prefix)
resp, aggregate = self.client.create_aggregate(
name=aggregate_name, availability_zone=az_name)
+ self.addCleanup(self._try_delete_aggregate, aggregate['id'])
self.assertEqual(200, resp.status)
self.assertEqual(aggregate_name, aggregate['name'])
self.assertEqual(az_name, aggregate['availability_zone'])
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index 1f2ddf4..49af645 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -14,7 +14,6 @@
from tempest.api.compute import base
from tempest.common.utils import data_utils
-from tempest import exceptions
from tempest import test
@@ -44,16 +43,6 @@
wait_until='ACTIVE')
cls.s2_id = server['id']
- def _get_unused_flavor_id(self):
- flavor_id = data_utils.rand_int_id(start=1000)
- while True:
- try:
- resp, body = self.flavors_client.get_flavor_details(flavor_id)
- except exceptions.NotFound:
- break
- flavor_id = data_utils.rand_int_id(start=1000)
- return flavor_id
-
@test.attr(type='gate')
def test_list_servers_by_admin(self):
# Listing servers by admin user returns empty list by default
diff --git a/tempest/api/compute/floating_ips/test_floating_ips_actions.py b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
index abd8a4c..b3789f8 100644
--- a/tempest/api/compute/floating_ips/test_floating_ips_actions.py
+++ b/tempest/api/compute/floating_ips/test_floating_ips_actions.py
@@ -45,6 +45,14 @@
resp, body = cls.client.delete_floating_ip(cls.floating_ip_id)
super(FloatingIPsTestJSON, cls).tearDownClass()
+ def _try_delete_floating_ip(self, floating_ip_id):
+ # delete floating ip, if it exists
+ try:
+ self.client.delete_floating_ip(floating_ip_id)
+ # if not found, it depicts it was deleted in the test
+ except exceptions.NotFound:
+ pass
+
@test.attr(type='gate')
def test_allocate_floating_ip(self):
# Positive test:Allocation of a new floating IP to a project
@@ -66,6 +74,7 @@
# should be successful
# Creating the floating IP that is to be deleted in this method
resp, floating_ip_body = self.client.create_floating_ip()
+ self.addCleanup(self._try_delete_floating_ip, floating_ip_body['id'])
# Storing the details of floating IP before deleting it
cli_resp = self.client.get_floating_ip_details(floating_ip_body['id'])
resp, floating_ip_details = cli_resp
diff --git a/tempest/api/compute/servers/test_list_server_filters.py b/tempest/api/compute/servers/test_list_server_filters.py
index f0913f1..f66020c 100644
--- a/tempest/api/compute/servers/test_list_server_filters.py
+++ b/tempest/api/compute/servers/test_list_server_filters.py
@@ -28,6 +28,7 @@
@classmethod
@test.safe_setup
def setUpClass(cls):
+ cls.set_network_resources(network=True, subnet=True, dhcp=True)
super(ListServerFiltersTestJSON, cls).setUpClass()
cls.client = cls.servers_client
@@ -69,8 +70,12 @@
resp, cls.s3 = cls.create_test_server(name=cls.s3_name,
flavor=cls.flavor_ref_alt,
wait_until='ACTIVE')
-
- cls.fixed_network_name = CONF.compute.fixed_network_name
+ if (CONF.service_available.neutron and
+ CONF.compute.allow_tenant_isolation):
+ network = cls.isolated_creds.get_primary_network()
+ cls.fixed_network_name = network['name']
+ else:
+ cls.fixed_network_name = CONF.compute.fixed_network_name
@utils.skip_unless_attr('multiple_images', 'Only one image found')
@test.attr(type='gate')
@@ -226,7 +231,6 @@
self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
- @test.skip_because(bug="1170718")
@test.attr(type='gate')
def test_list_servers_filtered_by_ip(self):
# Filter servers by ip
diff --git a/tempest/api/compute/servers/test_server_rescue_negative.py b/tempest/api/compute/servers/test_server_rescue_negative.py
index 65797e9..dae4709 100644
--- a/tempest/api/compute/servers/test_server_rescue_negative.py
+++ b/tempest/api/compute/servers/test_server_rescue_negative.py
@@ -12,12 +12,16 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest import config
from tempest import exceptions
from tempest import test
+CONF = config.CONF
+
class ServerRescueNegativeTestJSON(base.BaseV2ComputeTest):
@@ -67,6 +71,8 @@
self.assertEqual(202, resp.status)
self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_rescue_paused_instance(self):
# Rescue a paused server
diff --git a/tempest/api/compute/v3/admin/test_servers.py b/tempest/api/compute/v3/admin/test_servers.py
index 579a535..366cfc6 100644
--- a/tempest/api/compute/v3/admin/test_servers.py
+++ b/tempest/api/compute/v3/admin/test_servers.py
@@ -14,7 +14,6 @@
from tempest.api.compute import base
from tempest.common.utils import data_utils
-from tempest import exceptions
from tempest import test
@@ -44,16 +43,6 @@
wait_until='ACTIVE')
cls.s2_id = server['id']
- def _get_unused_flavor_id(self):
- flavor_id = data_utils.rand_int_id(start=1000)
- while True:
- try:
- resp, body = self.flavors_client.get_flavor_details(flavor_id)
- except exceptions.NotFound:
- break
- flavor_id = data_utils.rand_int_id(start=1000)
- return flavor_id
-
@test.attr(type='gate')
def test_list_servers_by_admin(self):
# Listing servers by admin user returns empty list by default
diff --git a/tempest/api/compute/v3/servers/test_delete_server.py b/tempest/api/compute/v3/servers/test_delete_server.py
index 8f85557..add69ab 100644
--- a/tempest/api/compute/v3/servers/test_delete_server.py
+++ b/tempest/api/compute/v3/servers/test_delete_server.py
@@ -56,6 +56,8 @@
self.assertEqual('204', resp['status'])
self.client.wait_for_server_termination(server['id'])
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type='gate')
def test_delete_server_while_in_pause_state(self):
# Delete a server while it's VM state is Pause
diff --git a/tempest/api/compute/v3/servers/test_list_server_filters.py b/tempest/api/compute/v3/servers/test_list_server_filters.py
index 2cb176c..778b033 100644
--- a/tempest/api/compute/v3/servers/test_list_server_filters.py
+++ b/tempest/api/compute/v3/servers/test_list_server_filters.py
@@ -28,6 +28,7 @@
@classmethod
@test.safe_setup
def setUpClass(cls):
+ cls.set_network_resources(network=True, subnet=True, dhcp=True)
super(ListServerFiltersV3Test, cls).setUpClass()
cls.client = cls.servers_client
@@ -70,7 +71,12 @@
flavor=cls.flavor_ref_alt,
wait_until='ACTIVE')
- cls.fixed_network_name = CONF.compute.fixed_network_name
+ if (CONF.service_available.neutron and
+ CONF.compute.allow_tenant_isolation):
+ network = cls.isolated_creds.get_primary_network()
+ cls.fixed_network_name = network['name']
+ else:
+ cls.fixed_network_name = CONF.compute.fixed_network_name
@utils.skip_unless_attr('multiple_images', 'Only one image found')
@test.attr(type='gate')
@@ -226,7 +232,6 @@
self.assertNotIn(self.s2_name, map(lambda x: x['name'], servers))
self.assertNotIn(self.s3_name, map(lambda x: x['name'], servers))
- @test.skip_because(bug="1170718")
@test.attr(type='gate')
def test_list_servers_filtered_by_ip(self):
# Filter servers by ip
diff --git a/tempest/api/compute/v3/servers/test_server_actions.py b/tempest/api/compute/v3/servers/test_server_actions.py
index b6764a5..1495cb7 100644
--- a/tempest/api/compute/v3/servers/test_server_actions.py
+++ b/tempest/api/compute/v3/servers/test_server_actions.py
@@ -330,6 +330,8 @@
self.wait_for(self._get_output)
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type='gate')
def test_pause_unpause_server(self):
resp, server = self.client.pause_server(self.server_id)
diff --git a/tempest/api/compute/v3/servers/test_server_rescue_negative.py b/tempest/api/compute/v3/servers/test_server_rescue_negative.py
index 08fb127..eb6bcdd 100644
--- a/tempest/api/compute/v3/servers/test_server_rescue_negative.py
+++ b/tempest/api/compute/v3/servers/test_server_rescue_negative.py
@@ -12,12 +12,16 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest import config
from tempest import exceptions
from tempest import test
+CONF = config.CONF
+
class ServerRescueNegativeV3Test(base.BaseV3ComputeTest):
@@ -66,6 +70,8 @@
self.assertEqual(202, resp.status)
self.servers_client.wait_for_server_status(server_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_rescue_paused_instance(self):
# Rescue a paused server
diff --git a/tempest/api/compute/v3/servers/test_servers_negative.py b/tempest/api/compute/v3/servers/test_servers_negative.py
index 586a52a..c1d1935 100644
--- a/tempest/api/compute/v3/servers/test_servers_negative.py
+++ b/tempest/api/compute/v3/servers/test_servers_negative.py
@@ -115,6 +115,8 @@
self.assertRaises(exceptions.NotFound, self.client.reboot,
nonexistent_server, 'SOFT')
+ @testtools.skipUnless(CONF.compute_feature_enabled.pause,
+ 'Pause is not available.')
@test.attr(type=['negative', 'gate'])
def test_pause_paused_server(self):
# Pause a paused server.
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 6bcc118..bc7f1d6 100644
--- a/tempest/api/network/admin/test_load_balancer_admin_actions.py
+++ b/tempest/api/network/admin/test_load_balancer_admin_actions.py
@@ -43,6 +43,8 @@
tenant_name)['id']
cls.network = cls.create_network()
cls.subnet = cls.create_subnet(cls.network)
+ cls.pool = cls.create_pool(data_utils.rand_name('pool-'),
+ "ROUND_ROBIN", "HTTP", cls.subnet)
@test.attr(type='smoke')
def test_create_vip_as_admin_for_another_tenant(self):
@@ -102,6 +104,17 @@
self.assertIsNotNone(pool['id'])
self.assertEqual(self.tenant_id, pool['tenant_id'])
+ @test.attr(type='smoke')
+ def test_create_member_from_admin_user_other_tenant(self):
+ resp, body = self.admin_client.create_member(
+ address="10.0.9.47", protocol_port=80, pool_id=self.pool['id'],
+ tenant_id=self.tenant_id)
+ self.assertEqual('201', resp['status'])
+ member = body['member']
+ self.addCleanup(self.admin_client.delete_member, member['id'])
+ self.assertIsNotNone(member['id'])
+ self.assertEqual(self.tenant_id, member['tenant_id'])
+
class LoadBalancerAdminTestXML(LoadBalancerAdminTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/test_load_balancer.py b/tempest/api/network/test_load_balancer.py
index 3ab015e..673fc47 100644
--- a/tempest/api/network/test_load_balancer.py
+++ b/tempest/api/network/test_load_balancer.py
@@ -434,6 +434,40 @@
self.assertEqual(member_id, body['member']['id'])
self.assertIsNotNone(body['member']['admin_state_up'])
+ @test.attr(type='smoke')
+ def test_update_pool_related_to_member(self):
+ # Create new pool
+ resp, body = self.client.create_pool(
+ name=data_utils.rand_name("pool-"),
+ lb_method='ROUND_ROBIN',
+ protocol='HTTP',
+ subnet_id=self.subnet['id'])
+ self.assertEqual('201', resp['status'])
+ new_pool = body['pool']
+ self.addCleanup(self.client.delete_pool, new_pool['id'])
+ # Update member with new pool's id
+ resp, body = self.client.update_member(self.member['id'],
+ pool_id=new_pool['id'])
+ self.assertEqual('200', resp['status'])
+ # Confirm with show that pool_id change
+ resp, body = self.client.show_member(self.member['id'])
+ member = body['member']
+ self.assertEqual(member['pool_id'], new_pool['id'])
+ # Update member with old pool id, this is needed for clean up
+ resp, body = self.client.update_member(self.member['id'],
+ pool_id=self.pool['id'])
+ self.assertEqual('200', resp['status'])
+
+ @test.attr(type='smoke')
+ def test_update_member_weight(self):
+ resp, _ = self.client.update_member(self.member['id'],
+ weight=2)
+ self.assertEqual('200', resp['status'])
+ resp, body = self.client.show_member(self.member['id'])
+ self.assertEqual('200', resp['status'])
+ member = body['member']
+ self.assertEqual(2, member['weight'])
+
class LoadBalancerTestXML(LoadBalancerTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/test_networks.py b/tempest/api/network/test_networks.py
index 0175de7..de44f4d 100644
--- a/tempest/api/network/test_networks.py
+++ b/tempest/api/network/test_networks.py
@@ -37,15 +37,9 @@
create a subnet for a tenant
list tenant's subnets
show a tenant subnet details
- port create
- port delete
- port list
- port show
- port update
network update
subnet update
delete a network also deletes its subnets
- create a port with no IP address associated with it
All subnet tests are run once with ipv4 and once with ipv6.
@@ -115,13 +109,13 @@
@test.attr(type='smoke')
def test_show_network_fields(self):
# Verify specific fields of a network
- field_list = [('fields', 'id'), ('fields', 'name'), ]
+ fields = ['id', 'name']
resp, body = self.client.show_network(self.network['id'],
- field_list=field_list)
+ fields=fields)
self.assertEqual('200', resp['status'])
network = body['network']
- self.assertEqual(len(network), len(field_list))
- for label, field_name in field_list:
+ self.assertEqual(sorted(network.keys()), sorted(fields))
+ for field_name in fields:
self.assertEqual(network[field_name], self.network[field_name])
@test.attr(type='smoke')
@@ -136,13 +130,13 @@
@test.attr(type='smoke')
def test_list_networks_fields(self):
# Verify specific fields of the networks
- resp, body = self.client.list_networks(fields='id')
+ fields = ['id', 'name']
+ resp, body = self.client.list_networks(fields=fields)
self.assertEqual('200', resp['status'])
networks = body['networks']
self.assertNotEmpty(networks, "Network list returned is empty")
for network in networks:
- self.assertEqual(len(network), 1)
- self.assertIn('id', network)
+ self.assertEqual(sorted(network.keys()), sorted(fields))
@test.attr(type='smoke')
def test_show_subnet(self):
@@ -158,13 +152,13 @@
@test.attr(type='smoke')
def test_show_subnet_fields(self):
# Verify specific fields of a subnet
- field_list = [('fields', 'id'), ('fields', 'cidr'), ]
+ fields = ['id', 'network_id']
resp, body = self.client.show_subnet(self.subnet['id'],
- field_list=field_list)
+ fields=fields)
self.assertEqual('200', resp['status'])
subnet = body['subnet']
- self.assertEqual(len(subnet), len(field_list))
- for label, field_name in field_list:
+ self.assertEqual(sorted(subnet.keys()), sorted(fields))
+ for field_name in fields:
self.assertEqual(subnet[field_name], self.subnet[field_name])
@test.attr(type='smoke')
@@ -179,13 +173,13 @@
@test.attr(type='smoke')
def test_list_subnets_fields(self):
# Verify specific fields of subnets
- resp, body = self.client.list_subnets(fields='id')
+ fields = ['id', 'network_id']
+ resp, body = self.client.list_subnets(fields=fields)
self.assertEqual('200', resp['status'])
subnets = body['subnets']
self.assertNotEmpty(subnets, "Subnet list returned is empty")
for subnet in subnets:
- self.assertEqual(len(subnet), 1)
- self.assertIn('id', subnet)
+ self.assertEqual(sorted(subnet.keys()), sorted(fields))
def _try_delete_network(self, net_id):
# delete network, if it exists
@@ -222,32 +216,6 @@
# it from the list.
self.subnets.pop()
- @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.
- name = data_utils.rand_name('network-nosubnet-')
- resp, body = self.client.create_network(name=name)
- self.assertEqual('201', resp['status'])
- network = body['network']
- net_id = network['id']
- self.networks.append(network)
-
- # Now create a port for this network - without creating any
- # subnets for this network - this ensures no IP for the port
- resp, body = self.client.create_port(network_id=net_id)
- self.assertEqual('201', resp['status'])
- port = body['port']
- port_id = port['id']
- self.addCleanup(self.client.delete_port, port_id)
-
- # Verify that the port does not have any IP address
- resp, body = self.client.show_port(port_id)
- self.assertEqual('200', resp['status'])
- port_resp = body['port']
- self.assertEqual(port_id, port_resp['id'])
- self.assertEqual(port_resp['fixed_ips'], [])
-
class NetworksTestXML(NetworksTestJSON):
_interface = 'xml'
diff --git a/tempest/api/network/test_ports.py b/tempest/api/network/test_ports.py
index bda5b2b..68f617b 100644
--- a/tempest/api/network/test_ports.py
+++ b/tempest/api/network/test_ports.py
@@ -26,6 +26,16 @@
class PortsTestJSON(base.BaseNetworkTest):
_interface = 'json'
+ """
+ Test the following operations for ports:
+
+ port create
+ port delete
+ port list
+ port show
+ port update
+ """
+
@classmethod
@test.safe_setup
def setUpClass(cls):
@@ -80,17 +90,18 @@
self.assertEqual(self.port['network_id'], port['network_id'])
self.assertEqual(self.port['security_groups'],
port['security_groups'])
+ self.assertEqual(port['fixed_ips'], [])
@test.attr(type='smoke')
def test_show_port_fields(self):
# Verify specific fields of a port
- field_list = [('fields', 'id'), ]
+ fields = ['id', 'mac_address']
resp, body = self.client.show_port(self.port['id'],
- field_list=field_list)
+ fields=fields)
self.assertEqual('200', resp['status'])
port = body['port']
- self.assertEqual(len(port), len(field_list))
- for label, field_name in field_list:
+ self.assertEqual(sorted(port.keys()), sorted(fields))
+ for field_name in fields:
self.assertEqual(port[field_name], self.port[field_name])
@test.attr(type='smoke')
@@ -126,14 +137,14 @@
@test.attr(type='smoke')
def test_list_ports_fields(self):
# Verify specific fields of ports
- resp, body = self.client.list_ports(fields='id')
+ fields = ['id', 'mac_address']
+ resp, body = self.client.list_ports(fields=fields)
self.assertEqual('200', resp['status'])
ports = body['ports']
self.assertNotEmpty(ports, "Port list returned is empty")
# Asserting the fields returned are correct
for port in ports:
- self.assertEqual(len(port), 1)
- self.assertIn('id', port)
+ self.assertEqual(sorted(fields), sorted(port.keys()))
class PortsTestXML(PortsTestJSON):
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index b057698..06e63a4 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -14,8 +14,10 @@
# under the License.
import hashlib
+import random
import re
from six import moves
+import time
from tempest.api.object_storage import base
from tempest.common import custom_matchers
@@ -308,10 +310,7 @@
# retrieve object's data (in response body)
# 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)
+ object_name, data = self._create_object()
# get object
resp, body = self.object_client.get_object(self.container_name,
object_name)
@@ -321,6 +320,183 @@
self.assertEqual(body, data)
@test.attr(type='smoke')
+ def test_get_object_with_metadata(self):
+ # get object with metadata
+ 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, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=None)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertIn('x-object-meta-test-meta', resp)
+ self.assertEqual(resp['x-object-meta-test-meta'], 'Meta')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_range(self):
+ # get object with range
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string(100)
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=None)
+ rand_num = random.randint(3, len(data) - 1)
+ metadata = {'Range': 'bytes=%s-%s' % (rand_num - 3, rand_num - 1)}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data[rand_num - 3: rand_num])
+
+ @test.attr(type='smoke')
+ def test_get_object_with_x_object_manifest(self):
+ # get object 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, body = self.object_client.create_object(
+ self.container_name,
+ object_name,
+ data_empty,
+ metadata=metadata)
+
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=None)
+ 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', 'GET'))
+ 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))
+
+ self.assertEqual(''.join(data_segments), body)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_if_match(self):
+ # get object with if_match
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string(10)
+ create_md5 = hashlib.md5(data).hexdigest()
+ create_metadata = {'Etag': create_md5}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ list_metadata = {'If-Match': create_md5}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_if_modified_since(self):
+ # get object with if_modified_since
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string()
+ time_now = time.time()
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=None)
+
+ http_date = time.ctime(time_now - 86400)
+ list_metadata = {'If-Modified-Since': http_date}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ def test_get_object_with_if_none_match(self):
+ # get object with if_none_match
+ object_name = data_utils.rand_name(name='TestObject')
+ data = data_utils.arbitrary_string(10)
+ create_md5 = hashlib.md5(data).hexdigest()
+ create_metadata = {'Etag': create_md5}
+ self.object_client.create_object(self.container_name,
+ object_name,
+ data,
+ metadata=create_metadata)
+
+ list_data = data_utils.arbitrary_string(15)
+ list_md5 = hashlib.md5(list_data).hexdigest()
+ list_metadata = {'If-None-Match': list_md5}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_if_unmodified_since(self):
+ # get object with if_unmodified_since
+ object_name, data = self._create_object()
+
+ time_now = time.time()
+ http_date = time.ctime(time_now + 86400)
+ list_metadata = {'If-Unmodified-Since': http_date}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
+ def test_get_object_with_x_newest(self):
+ # get object with x_newest
+ object_name, data = self._create_object()
+
+ list_metadata = {'X-Newest': 'true'}
+ resp, body = self.object_client.get_object(
+ self.container_name,
+ object_name,
+ metadata=list_metadata)
+ self.assertIn(int(resp['status']), test.HTTP_SUCCESS)
+ self.assertHeaders(resp, 'Object', 'GET')
+ self.assertEqual(body, data)
+
+ @test.attr(type='smoke')
def test_copy_object_in_same_container(self):
# create source object
src_object_name = data_utils.rand_name(name='SrcObject')
@@ -498,10 +674,7 @@
# Make a conditional request for an object using the If-None-Match
# header, it should get downloaded only if the local file is different,
# otherwise the response code should be 304 Not Modified
- object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- self.object_client.create_object(self.container_name,
- object_name, data)
+ object_name, data = self._create_object()
# local copy is identical, no download
md5 = hashlib.md5(data).hexdigest()
headers = {'If-None-Match': md5}
diff --git a/tempest/api/orchestration/base.py b/tempest/api/orchestration/base.py
index 7656ff3..c27bedf 100644
--- a/tempest/api/orchestration/base.py
+++ b/tempest/api/orchestration/base.py
@@ -101,9 +101,8 @@
@classmethod
def load_template(cls, name, ext='yaml'):
- loc = ["tempest", "api", "orchestration",
- "stacks", "templates", "%s.%s" % (name, ext)]
- fullpath = os.path.join(*loc)
+ loc = ["stacks", "templates", "%s.%s" % (name, ext)]
+ fullpath = os.path.join(os.path.dirname(__file__), *loc)
with open(fullpath, "r") as f:
content = f.read()
diff --git a/tempest/api_schema/compute/aggregates.py b/tempest/api_schema/compute/aggregates.py
index a70b356..a3ab3c8 100644
--- a/tempest/api_schema/compute/aggregates.py
+++ b/tempest/api_schema/compute/aggregates.py
@@ -12,9 +12,11 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
+
aggregate = {
'type': 'object',
- 'properties:': {
+ 'properties': {
'availability_zone': {'type': ['string', 'null']},
'created_at': {'type': 'string'},
'deleted': {'type': 'boolean'},
@@ -56,3 +58,9 @@
}
aggregate_set_metadata = get_aggregate
+# The 'updated_at' attribute of 'update_aggregate' can't be null.
+update_aggregate = copy.deepcopy(get_aggregate)
+update_aggregate['response_body']['properties']['aggregate']['properties'][
+ 'updated_at'] = {
+ 'type': 'string'
+ }
diff --git a/tempest/api_schema/compute/availability_zone.py b/tempest/api_schema/compute/availability_zone.py
new file mode 100644
index 0000000..c1abc64
--- /dev/null
+++ b/tempest/api_schema/compute/availability_zone.py
@@ -0,0 +1,37 @@
+# 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.
+
+# NOTE: This is the detail information for "get az detail" API.
+# The information is the same between v2 and v3 APIs.
+detail = {
+ 'type': 'object',
+ 'patternProperties': {
+ # NOTE: Here is for a hostname
+ '^[a-zA-Z0-9-_.]+$': {
+ 'type': 'object',
+ 'patternProperties': {
+ # NOTE: Here is for a service name
+ '^.*$': {
+ 'type': 'object',
+ 'properties': {
+ 'available': {'type': 'boolean'},
+ 'active': {'type': 'boolean'},
+ 'updated_at': {'type': 'string'}
+ },
+ 'required': ['available', 'active', 'updated_at']
+ }
+ }
+ }
+ }
+}
diff --git a/tempest/api_schema/compute/flavors_extra_specs.py b/tempest/api_schema/compute/flavors_extra_specs.py
new file mode 100644
index 0000000..4003d36
--- /dev/null
+++ b/tempest/api_schema/compute/flavors_extra_specs.py
@@ -0,0 +1,39 @@
+# 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.
+
+flavor_extra_specs = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'extra_specs': {
+ 'type': 'object',
+ 'patternProperties': {
+ '^[a-zA-Z0-9_\-\. :]+$': {'type': 'string'}
+ }
+ }
+ },
+ 'required': ['extra_specs']
+ }
+}
+
+flavor_extra_specs_key = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'patternProperties': {
+ '^[a-zA-Z0-9_\-\. :]+$': {'type': 'string'}
+ }
+ }
+}
diff --git a/tempest/api_schema/compute/interfaces.py b/tempest/api_schema/compute/interfaces.py
new file mode 100644
index 0000000..1e15c18
--- /dev/null
+++ b/tempest/api_schema/compute/interfaces.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_interface = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/servers.py b/tempest/api_schema/compute/servers.py
index 5eb5ecb..a273abb 100644
--- a/tempest/api_schema/compute/servers.py
+++ b/tempest/api_schema/compute/servers.py
@@ -43,3 +43,7 @@
'required': ['console']
}
}
+
+delete_server = {
+ 'status_code': [204],
+}
diff --git a/tempest/api_schema/compute/v2/availability_zone.py b/tempest/api_schema/compute/v2/availability_zone.py
new file mode 100644
index 0000000..d3d2787
--- /dev/null
+++ b/tempest/api_schema/compute/v2/availability_zone.py
@@ -0,0 +1,54 @@
+# 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 availability_zone as common
+
+
+base = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'availabilityZoneInfo': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'zoneName': {'type': 'string'},
+ 'zoneState': {
+ 'type': 'object',
+ 'properties': {
+ 'available': {'type': 'boolean'}
+ },
+ 'required': ['available']
+ },
+ # NOTE: Here is the difference between detail and
+ # non-detail.
+ 'hosts': {'type': 'null'}
+ },
+ 'required': ['zoneName', 'zoneState', 'hosts']
+ }
+ }
+ },
+ 'required': ['availabilityZoneInfo']
+ }
+}
+
+get_availability_zone_list = copy.deepcopy(base)
+
+get_availability_zone_list_detail = copy.deepcopy(base)
+get_availability_zone_list_detail['response_body']['properties'][
+ 'availabilityZoneInfo']['items']['properties']['hosts'] = common.detail
diff --git a/tempest/api_schema/compute/v2/flavors.py b/tempest/api_schema/compute/v2/flavors.py
index 999ca19..48e6ceb 100644
--- a/tempest/api_schema/compute/v2/flavors.py
+++ b/tempest/api_schema/compute/v2/flavors.py
@@ -31,3 +31,7 @@
'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'.
+
+unset_flavor_extra_specs = {
+ 'status_code': [200]
+}
diff --git a/tempest/api_schema/compute/v2/servers.py b/tempest/api_schema/compute/v2/servers.py
index 81d2ef7..eed4589 100644
--- a/tempest/api_schema/compute/v2/servers.py
+++ b/tempest/api_schema/compute/v2/servers.py
@@ -66,3 +66,27 @@
'required': ['virtual_interfaces']
}
}
+
+attach_volume = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'volumeAttachment': {
+ 'type': 'object',
+ 'properties': {
+ 'id': {'type': 'string'},
+ 'device': {'type': 'string'},
+ 'volumeId': {'type': 'string'},
+ 'serverId': {'type': ['integer', 'string']}
+ },
+ 'required': ['id', 'device', 'volumeId', 'serverId']
+ }
+ },
+ 'required': ['volumeAttachment']
+ }
+}
+
+detach_volume = {
+ 'status_code': [202]
+}
diff --git a/tempest/api_schema/compute/v3/availability_zone.py b/tempest/api_schema/compute/v3/availability_zone.py
new file mode 100644
index 0000000..5f36c33
--- /dev/null
+++ b/tempest/api_schema/compute/v3/availability_zone.py
@@ -0,0 +1,53 @@
+# 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 availability_zone as common
+
+
+base = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'availability_zone_info': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'zone_name': {'type': 'string'},
+ 'zone_state': {
+ 'type': 'object',
+ 'properties': {
+ 'available': {'type': 'boolean'}
+ },
+ 'required': ['available']
+ },
+ # NOTE: Here is the difference between detail and
+ # non-detail
+ 'hosts': {'type': 'null'}
+ },
+ 'required': ['zone_name', 'zone_state', 'hosts']
+ }
+ }
+ },
+ 'required': ['availability_zone_info']
+ }
+}
+
+get_availability_zone_list = copy.deepcopy(base)
+get_availability_zone_list_detail = copy.deepcopy(base)
+get_availability_zone_list_detail['response_body']['properties'][
+ 'availability_zone_info']['items']['properties']['hosts'] = common.detail
diff --git a/tempest/api_schema/compute/v3/flavors.py b/tempest/api_schema/compute/v3/flavors.py
index 542d2b1..468658c 100644
--- a/tempest/api_schema/compute/v3/flavors.py
+++ b/tempest/api_schema/compute/v3/flavors.py
@@ -15,6 +15,7 @@
import copy
from tempest.api_schema.compute import flavors
+from tempest.api_schema.compute import flavors_extra_specs
list_flavors_details = copy.deepcopy(flavors.common_flavor_list_details)
@@ -31,3 +32,10 @@
# So they are not 'required'.
list_flavors_details['response_body']['properties']['flavors']['items'][
'required'].extend(['disabled', 'ephemeral'])
+
+set_flavor_extra_specs = copy.deepcopy(flavors_extra_specs.flavor_extra_specs)
+set_flavor_extra_specs['status_code'] = [201]
+
+unset_flavor_extra_specs = {
+ 'status_code': [204]
+}
diff --git a/tempest/api_schema/compute/v3/servers.py b/tempest/api_schema/compute/v3/servers.py
index 390962e..f2a4b78 100644
--- a/tempest/api_schema/compute/v3/servers.py
+++ b/tempest/api_schema/compute/v3/servers.py
@@ -42,3 +42,7 @@
'required': ['server']
}
}
+
+attach_detach_volume = {
+ 'status_code': [202]
+}
diff --git a/tempest/common/generator/base_generator.py b/tempest/common/generator/base_generator.py
index 87d65d0..57b98f7 100644
--- a/tempest/common/generator/base_generator.py
+++ b/tempest/common/generator/base_generator.py
@@ -123,7 +123,7 @@
raise Exception("non-integer list types not supported")
result = []
if schema_type not in self.types_dict:
- raise Exception("generator (%s) doesn't support type: %s"
+ raise TypeError("generator (%s) doesn't support type: %s"
% (self.__class__.__name__, schema_type))
for generator in self.types_dict[schema_type]:
ret = generator(schema)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 5895c37..1e7ddb1 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -19,7 +19,6 @@
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
@@ -490,6 +489,8 @@
def wait_node(self, instance_id):
"""Waits for a node to be associated with instance_id."""
+ from ironicclient import exc as ironic_exceptions
+
def _get_node():
node = None
try:
diff --git a/tempest/scenario/test_security_groups_basic_ops.py b/tempest/scenario/test_security_groups_basic_ops.py
index b9ee040..b1b06cc 100644
--- a/tempest/scenario/test_security_groups_basic_ops.py
+++ b/tempest/scenario/test_security_groups_basic_ops.py
@@ -422,11 +422,15 @@
access_point_ssh = self._connect_to_access_point(tenant)
mac_addr = access_point_ssh.get_mac_address()
mac_addr = mac_addr.strip().lower()
- port_list = self.network_client.list_ports()['ports']
+ # Get the fixed_ips and mac_address fields of all ports. Select
+ # only those two columns to reduce the size of the response.
+ port_list = self.network_client.list_ports(
+ fields=['fixed_ips', 'mac_address'])['ports']
port_detail_list = [
(port['fixed_ips'][0]['subnet_id'],
port['fixed_ips'][0]['ip_address'],
- port['mac_address'].lower()) for port in port_list
+ port['mac_address'].lower())
+ for port in port_list if port['fixed_ips']
]
server_ip = self._get_server_ip(tenant.access_point)
subnet_id = tenant.subnet.id
diff --git a/tempest/services/baremetal/v1/base_v1.py b/tempest/services/baremetal/v1/base_v1.py
index 2145c88..296a199 100644
--- a/tempest/services/baremetal/v1/base_v1.py
+++ b/tempest/services/baremetal/v1/base_v1.py
@@ -42,6 +42,11 @@
return self._list_request('ports', **kwargs)
@base.handle_errors
+ def list_nodestates(self, uuid):
+ """List all existing states."""
+ return self._list_request('/nodes/%s/states' % uuid)
+
+ @base.handle_errors
def list_ports_detail(self):
"""Details list all existing ports."""
return self._list_request('/ports/detail')
diff --git a/tempest/services/compute/json/aggregates_client.py b/tempest/services/compute/json/aggregates_client.py
index fe67102..54d1252 100644
--- a/tempest/services/compute/json/aggregates_client.py
+++ b/tempest/services/compute/json/aggregates_client.py
@@ -61,6 +61,7 @@
resp, body = self.put('os-aggregates/%s' % str(aggregate_id), put_body)
body = json.loads(body)
+ self.validate_response(schema.update_aggregate, resp, body)
return resp, body['aggregate']
def delete_aggregate(self, aggregate_id):
diff --git a/tempest/services/compute/json/availability_zone_client.py b/tempest/services/compute/json/availability_zone_client.py
index 9278d5b..1c067e8 100644
--- a/tempest/services/compute/json/availability_zone_client.py
+++ b/tempest/services/compute/json/availability_zone_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute.v2 import availability_zone as schema
from tempest.common import rest_client
from tempest import config
@@ -31,9 +32,12 @@
def get_availability_zone_list(self):
resp, body = self.get('os-availability-zone')
body = json.loads(body)
+ self.validate_response(schema.get_availability_zone_list, resp, body)
return resp, body['availabilityZoneInfo']
def get_availability_zone_list_detail(self):
resp, body = self.get('os-availability-zone/detail')
body = json.loads(body)
+ self.validate_response(schema.get_availability_zone_list_detail, resp,
+ body)
return resp, body['availabilityZoneInfo']
diff --git a/tempest/services/compute/json/flavors_client.py b/tempest/services/compute/json/flavors_client.py
index 0206b82..65d2657 100644
--- a/tempest/services/compute/json/flavors_client.py
+++ b/tempest/services/compute/json/flavors_client.py
@@ -18,6 +18,8 @@
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 import flavors_extra_specs \
+ as schema_extra_specs
from tempest.api_schema.compute.v2 import flavors as v2schema
from tempest.common import rest_client
from tempest import config
@@ -99,12 +101,16 @@
resp, body = self.post('flavors/%s/os-extra_specs' % flavor_id,
post_body)
body = json.loads(body)
+ self.validate_response(schema_extra_specs.flavor_extra_specs,
+ resp, body)
return resp, body['extra_specs']
def get_flavor_extra_spec(self, flavor_id):
"""Gets extra Specs details of the mentioned flavor."""
resp, body = self.get('flavors/%s/os-extra_specs' % flavor_id)
body = json.loads(body)
+ self.validate_response(schema_extra_specs.flavor_extra_specs,
+ resp, body)
return resp, body['extra_specs']
def get_flavor_extra_spec_with_key(self, flavor_id, key):
@@ -112,6 +118,8 @@
resp, body = self.get('flavors/%s/os-extra_specs/%s' % (str(flavor_id),
key))
body = json.loads(body)
+ self.validate_response(schema_extra_specs.flavor_extra_specs_key,
+ resp, body)
return resp, body
def update_flavor_extra_spec(self, flavor_id, key, **kwargs):
@@ -119,12 +127,16 @@
resp, body = self.put('flavors/%s/os-extra_specs/%s' %
(flavor_id, key), json.dumps(kwargs))
body = json.loads(body)
+ self.validate_response(schema_extra_specs.flavor_extra_specs_key,
+ resp, body)
return resp, body
def unset_flavor_extra_spec(self, flavor_id, key):
"""Unsets extra Specs from the mentioned flavor."""
- return self.delete('flavors/%s/os-extra_specs/%s' % (str(flavor_id),
- key))
+ resp, body = self.delete('flavors/%s/os-extra_specs/%s' %
+ (str(flavor_id), key))
+ self.validate_response(v2schema.unset_flavor_extra_specs, resp, body)
+ return resp, body
def list_flavor_access(self, flavor_id):
"""Gets flavor access information given the flavor id."""
diff --git a/tempest/services/compute/json/interfaces_client.py b/tempest/services/compute/json/interfaces_client.py
index 9928b94..2f165a2 100644
--- a/tempest/services/compute/json/interfaces_client.py
+++ b/tempest/services/compute/json/interfaces_client.py
@@ -16,6 +16,7 @@
import json
import time
+from tempest.api_schema.compute import interfaces as common_schema
from tempest.common import rest_client
from tempest import config
from tempest import exceptions
@@ -58,6 +59,7 @@
def delete_interface(self, server, port_id):
resp, body = self.delete('servers/%s/os-interface/%s' % (server,
port_id))
+ self.validate_response(common_schema.delete_interface, resp, body)
return resp, body
def wait_for_interface_status(self, server, port_id, status):
diff --git a/tempest/services/compute/json/servers_client.py b/tempest/services/compute/json/servers_client.py
index cf1dad9..70a950a 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -136,7 +136,9 @@
def delete_server(self, server_id):
"""Deletes the given server."""
- return self.delete("servers/%s" % str(server_id))
+ resp, body = self.delete("servers/%s" % str(server_id))
+ self.validate_response(common_schema.delete_server, resp, body)
+ return resp, body
def list_servers(self, params=None):
"""Lists all servers for a user."""
@@ -326,12 +328,15 @@
})
resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
post_body)
+ body = json.loads(body)
+ self.validate_response(schema.attach_volume, resp, body)
return resp, body
def detach_volume(self, server_id, volume_id):
"""Detaches a volume from a server instance."""
resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
(server_id, volume_id))
+ self.validate_response(schema.detach_volume, resp, body)
return resp, body
def add_security_group(self, server_id, name):
diff --git a/tempest/services/compute/v3/json/aggregates_client.py b/tempest/services/compute/v3/json/aggregates_client.py
index 8d7440e..0fc6af9 100644
--- a/tempest/services/compute/v3/json/aggregates_client.py
+++ b/tempest/services/compute/v3/json/aggregates_client.py
@@ -61,6 +61,7 @@
resp, body = self.put('os-aggregates/%s' % str(aggregate_id), put_body)
body = json.loads(body)
+ self.validate_response(schema.update_aggregate, resp, body)
return resp, body['aggregate']
def delete_aggregate(self, aggregate_id):
diff --git a/tempest/services/compute/v3/json/availability_zone_client.py b/tempest/services/compute/v3/json/availability_zone_client.py
index bad2de9..bf74e68 100644
--- a/tempest/services/compute/v3/json/availability_zone_client.py
+++ b/tempest/services/compute/v3/json/availability_zone_client.py
@@ -15,6 +15,7 @@
import json
+from tempest.api_schema.compute.v3 import availability_zone as schema
from tempest.common import rest_client
from tempest import config
@@ -31,9 +32,12 @@
def get_availability_zone_list(self):
resp, body = self.get('os-availability-zone')
body = json.loads(body)
+ self.validate_response(schema.get_availability_zone_list, resp, body)
return resp, body['availability_zone_info']
def get_availability_zone_list_detail(self):
resp, body = self.get('os-availability-zone/detail')
body = json.loads(body)
+ self.validate_response(schema.get_availability_zone_list_detail, resp,
+ body)
return resp, body['availability_zone_info']
diff --git a/tempest/services/compute/v3/json/flavors_client.py b/tempest/services/compute/v3/json/flavors_client.py
index 189fe3f..602fee2 100644
--- a/tempest/services/compute/v3/json/flavors_client.py
+++ b/tempest/services/compute/v3/json/flavors_client.py
@@ -18,6 +18,8 @@
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 import flavors_extra_specs \
+ as schema_extra_specs
from tempest.api_schema.compute.v3 import flavors as v3schema
from tempest.common import rest_client
from tempest import config
@@ -99,12 +101,15 @@
resp, body = self.post('flavors/%s/flavor-extra-specs' % flavor_id,
post_body)
body = json.loads(body)
+ self.validate_response(v3schema.set_flavor_extra_specs, resp, body)
return resp, body['extra_specs']
def get_flavor_extra_spec(self, flavor_id):
"""Gets extra Specs details of the mentioned flavor."""
resp, body = self.get('flavors/%s/flavor-extra-specs' % flavor_id)
body = json.loads(body)
+ self.validate_response(schema_extra_specs.flavor_extra_specs,
+ resp, body)
return resp, body['extra_specs']
def get_flavor_extra_spec_with_key(self, flavor_id, key):
@@ -112,6 +117,8 @@
resp, body = self.get('flavors/%s/flavor-extra-specs/%s' %
(str(flavor_id), key))
body = json.loads(body)
+ self.validate_response(schema_extra_specs.flavor_extra_specs_key,
+ resp, body)
return resp, body
def update_flavor_extra_spec(self, flavor_id, key, **kwargs):
@@ -119,12 +126,16 @@
resp, body = self.put('flavors/%s/flavor-extra-specs/%s' %
(flavor_id, key), json.dumps(kwargs))
body = json.loads(body)
+ self.validate_response(schema_extra_specs.flavor_extra_specs_key,
+ resp, body)
return resp, body
def unset_flavor_extra_spec(self, flavor_id, key):
"""Unsets extra Specs from the mentioned flavor."""
- return self.delete('flavors/%s/flavor-extra-specs/%s' %
- (str(flavor_id), key))
+ resp, body = self.delete('flavors/%s/flavor-extra-specs/%s' %
+ (str(flavor_id), key))
+ self.validate_response(v3schema.unset_flavor_extra_specs, resp, body)
+ return resp, body
def list_flavor_access(self, flavor_id):
"""Gets flavor access information given the flavor id."""
diff --git a/tempest/services/compute/v3/json/interfaces_client.py b/tempest/services/compute/v3/json/interfaces_client.py
index b45426c..25c8db7 100644
--- a/tempest/services/compute/v3/json/interfaces_client.py
+++ b/tempest/services/compute/v3/json/interfaces_client.py
@@ -16,6 +16,7 @@
import json
import time
+from tempest.api_schema.compute import interfaces as common_schema
from tempest.common import rest_client
from tempest import config
from tempest import exceptions
@@ -59,6 +60,7 @@
resp, body =\
self.delete('servers/%s/os-attach-interfaces/%s' % (server,
port_id))
+ self.validate_response(common_schema.delete_interface, resp, body)
return resp, body
def wait_for_interface_status(self, server, port_id, status):
diff --git a/tempest/services/compute/v3/json/servers_client.py b/tempest/services/compute/v3/json/servers_client.py
index caeb315..bbffc13 100644
--- a/tempest/services/compute/v3/json/servers_client.py
+++ b/tempest/services/compute/v3/json/servers_client.py
@@ -136,7 +136,9 @@
def delete_server(self, server_id):
"""Deletes the given server."""
- return self.delete("servers/%s" % str(server_id))
+ resp, body = self.delete("servers/%s" % str(server_id))
+ self.validate_response(common_schema.delete_server, resp, body)
+ return resp, body
def list_servers(self, params=None):
"""Lists all servers for a user."""
@@ -326,12 +328,17 @@
def attach_volume(self, server_id, volume_id, device='/dev/vdz'):
"""Attaches a volume to a server instance."""
- return self.action(server_id, 'attach', None, volume_id=volume_id,
- device=device)
+ resp, body = self.action(server_id, 'attach', None,
+ volume_id=volume_id, device=device)
+ self.validate_response(schema.attach_detach_volume, resp, body)
+ return resp, body
def detach_volume(self, server_id, volume_id):
"""Detaches a volume from a server instance."""
- return self.action(server_id, 'detach', None, volume_id=volume_id)
+ resp, body = self.action(server_id, 'detach', None,
+ volume_id=volume_id)
+ self.validate_response(schema.attach_detach_volume, resp, body)
+ return resp, body
def live_migrate_server(self, server_id, dest_host, use_block_migration):
"""This should be called with administrator privileges ."""
diff --git a/tempest/services/compute/xml/aggregates_client.py b/tempest/services/compute/xml/aggregates_client.py
index b5f7678..9c2d4aa 100644
--- a/tempest/services/compute/xml/aggregates_client.py
+++ b/tempest/services/compute/xml/aggregates_client.py
@@ -61,9 +61,11 @@
def create_aggregate(self, name, availability_zone=None):
"""Creates a new aggregate."""
- post_body = xml_utils.Element("aggregate",
- name=name,
- availability_zone=availability_zone)
+ if availability_zone is not None:
+ post_body = xml_utils.Element("aggregate", name=name,
+ availability_zone=availability_zone)
+ else:
+ post_body = xml_utils.Element("aggregate", name=name)
resp, body = self.post('os-aggregates',
str(xml_utils.Document(post_body)))
aggregate = self._format_aggregate(etree.fromstring(body))
@@ -71,9 +73,11 @@
def update_aggregate(self, aggregate_id, name, availability_zone=None):
"""Update a aggregate."""
- put_body = xml_utils.Element("aggregate",
- name=name,
- availability_zone=availability_zone)
+ if availability_zone is not None:
+ put_body = xml_utils.Element("aggregate", name=name,
+ availability_zone=availability_zone)
+ else:
+ put_body = xml_utils.Element("aggregate", name=name)
resp, body = self.put('os-aggregates/%s' % str(aggregate_id),
str(xml_utils.Document(put_body)))
aggregate = self._format_aggregate(etree.fromstring(body))
diff --git a/tempest/services/network/network_client_base.py b/tempest/services/network/network_client_base.py
index 34c61b0..2a797b2 100644
--- a/tempest/services/network/network_client_base.py
+++ b/tempest/services/network/network_client_base.py
@@ -104,7 +104,7 @@
def _list(**filters):
uri = self.get_uri(plural_name)
if filters:
- uri += '?' + urllib.urlencode(filters)
+ uri += '?' + urllib.urlencode(filters, doseq=1)
resp, body = self.get(uri)
result = {plural_name: self.deserialize_list(body)}
return resp, result
@@ -120,14 +120,14 @@
return _delete
def _shower(self, resource_name):
- def _show(resource_id, field_list=[]):
- # field_list is a sequence of two-element tuples, with the
- # first element being 'fields'. An example:
- # [('fields', 'id'), ('fields', 'name')]
+ def _show(resource_id, **fields):
+ # fields is a dict which key is 'fields' and value is a
+ # list of field's name. An example:
+ # {'fields': ['id', 'name']}
plural = self.pluralize(resource_name)
uri = '%s/%s' % (self.get_uri(plural), resource_id)
- if field_list:
- uri += '?' + urllib.urlencode(field_list)
+ if fields:
+ uri += '?' + urllib.urlencode(fields, doseq=1)
resp, body = self.get(uri)
body = self.deserialize_single(body)
return resp, body
diff --git a/tempest/services/object_storage/object_client.py b/tempest/services/object_storage/object_client.py
index 53a3325..f3f4eb6 100644
--- a/tempest/services/object_storage/object_client.py
+++ b/tempest/services/object_storage/object_client.py
@@ -77,11 +77,16 @@
resp, body = self.head(url)
return resp, body
- def get_object(self, container, object_name):
+ def get_object(self, container, object_name, metadata=None):
"""Retrieve object's data."""
+ headers = {}
+ if metadata:
+ for key in metadata:
+ headers[str(key)] = metadata[key]
+
url = "{0}/{1}".format(container, object_name)
- resp, body = self.get(url)
+ resp, body = self.get(url, headers=headers)
return resp, body
def copy_object_in_same_container(self, container, src_object_name,
diff --git a/tempest/test.py b/tempest/test.py
index 8df405c..254fffa 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -75,12 +75,16 @@
try:
f(cls)
except Exception as se:
+ etype, value, trace = sys.exc_info()
LOG.exception("setUpClass failed: %s" % se)
try:
cls.tearDownClass()
except Exception as te:
LOG.exception("tearDownClass failed: %s" % te)
- raise se
+ try:
+ raise etype(value), None, trace
+ finally:
+ del trace # for avoiding circular refs
return decorator
diff --git a/tempest/tests/negative/test_generate_json.py b/tempest/tests/negative/test_generate_json.py
deleted file mode 100644
index e09fcdf..0000000
--- a/tempest/tests/negative/test_generate_json.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright 2014 Deutsche Telekom AG
-# 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.common.generator import negative_generator
-import tempest.test
-
-
-class TestNegativeGenerator(tempest.test.BaseTestCase):
-
- fake_input_str = {"type": "string",
- "minLength": 2,
- "maxLength": 8,
- 'results': {'gen_number': 404}}
-
- fake_input_int = {"type": "integer",
- "maximum": 255,
- "minimum": 1}
-
- fake_input_obj = {"type": "object",
- "properties": {"minRam": {"type": "integer"},
- "diskName": {"type": "string"},
- "maxRam": {"type": "integer", }
- }
- }
-
- def setUp(self):
- super(TestNegativeGenerator, self).setUp()
- self.negative = negative_generator.NegativeTestGenerator()
-
- def _validate_result(self, data):
- self.assertTrue(isinstance(data, list))
- for t in data:
- self.assertTrue(isinstance(t, tuple))
-
- def test_generate_invalid_string(self):
- result = self.negative.generate(self.fake_input_str)
- self._validate_result(result)
-
- def test_generate_invalid_integer(self):
- result = self.negative.generate(self.fake_input_int)
- self._validate_result(result)
-
- def test_generate_invalid_obj(self):
- result = self.negative.generate(self.fake_input_obj)
- self._validate_result(result)
diff --git a/tempest/tests/negative/test_negative_generators.py b/tempest/tests/negative/test_negative_generators.py
index f2ed999..c77faca 100644
--- a/tempest/tests/negative/test_negative_generators.py
+++ b/tempest/tests/negative/test_negative_generators.py
@@ -16,7 +16,9 @@
import jsonschema
import mock
-import tempest.common.generator.base_generator as base_generator
+from tempest.common.generator import base_generator
+from tempest.common.generator import negative_generator
+from tempest.common.generator import valid_generator
from tempest.tests import base
@@ -79,3 +81,73 @@
self.assertRaises(jsonschema.SchemaError,
self.generator.validate_schema,
self.invalid_json_schema_desc)
+
+
+class BaseNegativeGenerator(object):
+ types = ['string', 'integer', 'object']
+
+ fake_input_str = {"type": "string",
+ "minLength": 2,
+ "maxLength": 8,
+ 'results': {'gen_int': 404}}
+
+ fake_input_int = {"type": "integer",
+ "maximum": 255,
+ "minimum": 1}
+
+ fake_input_obj = {"type": "object",
+ "properties": {"minRam": {"type": "integer"},
+ "diskName": {"type": "string"},
+ "maxRam": {"type": "integer", }
+ }
+ }
+
+ unkown_type_schema = {
+ "type": "not_defined"
+ }
+
+ def _validate_result(self, data):
+ self.assertTrue(isinstance(data, list))
+ for t in data:
+ self.assertIsInstance(t, tuple)
+ self.assertEqual(3, len(t))
+ self.assertIsInstance(t[0], str)
+
+ def test_generate_string(self):
+ result = self.generator.generate(self.fake_input_str)
+ self._validate_result(result)
+
+ def test_generate_integer(self):
+ result = self.generator.generate(self.fake_input_int)
+ self._validate_result(result)
+
+ def test_generate_obj(self):
+ result = self.generator.generate(self.fake_input_obj)
+ self._validate_result(result)
+
+ def test_generator_mandatory_functions(self):
+ for data_type in self.types:
+ self.assertIn(data_type, self.generator.types_dict)
+
+ def test_generate_with_unknown_type(self):
+ self.assertRaises(TypeError, self.generator.generate,
+ self.unkown_type_schema)
+
+
+class TestNegativeValidGenerator(base.TestCase, BaseNegativeGenerator):
+ def setUp(self):
+ super(TestNegativeValidGenerator, self).setUp()
+ self.generator = valid_generator.ValidTestGenerator()
+
+ def test_generate_valid(self):
+ result = self.generator.generate_valid(self.fake_input_obj)
+ self.assertIn("minRam", result)
+ self.assertIsInstance(result["minRam"], int)
+ self.assertIn("diskName", result)
+ self.assertIsInstance(result["diskName"], str)
+
+
+class TestNegativeNegativeGenerator(base.TestCase, BaseNegativeGenerator):
+ def setUp(self):
+ super(TestNegativeNegativeGenerator, self).setUp()
+ self.generator = negative_generator.NegativeTestGenerator()
diff --git a/tempest/thirdparty/boto/test_ec2_instance_run.py b/tempest/thirdparty/boto/test_ec2_instance_run.py
index e8610d3..33b8d6e 100644
--- a/tempest/thirdparty/boto/test_ec2_instance_run.py
+++ b/tempest/thirdparty/boto/test_ec2_instance_run.py
@@ -218,10 +218,8 @@
else:
self.assertNotEqual(instance.state, "running")
- # NOTE(afazekas): doctored test case,
- # with normal validation it would fail
@test.attr(type='smoke')
- def test_integration_1(self):
+ def test_compute_with_volumes(self):
# EC2 1. integration test (not strict)
image_ami = self.ec2_client.get_image(self.images["ami"]["image_id"])
sec_group_name = data_utils.rand_name("securitygroup-")
@@ -249,14 +247,20 @@
instance_type=self.instance_type,
key_name=self.keypair_name,
security_groups=(sec_group_name,))
+
+ LOG.debug("Instance booted - state: %s",
+ reservation.instances[0].state)
+
self.addResourceCleanUp(self.destroy_reservation,
reservation)
volume = self.ec2_client.create_volume(1, self.zone)
+ LOG.debug("Volume created - status: %s", volume.status)
+
self.addResourceCleanUp(self.destroy_volume_wait, volume)
instance = reservation.instances[0]
- LOG.info("state: %s", instance.state)
if instance.state != "running":
self.assertInstanceStateWait(instance, "running")
+ LOG.debug("Instance now running - state: %s", instance.state)
address = self.ec2_client.allocate_address()
rcuk_a = self.addResourceCleanUp(address.delete)
@@ -284,10 +288,21 @@
volume.attach(instance.id, "/dev/vdh")
def _volume_state():
+ """Return volume state realizing that 'in-use' is overloaded."""
volume.update(validate=True)
- return volume.status
+ status = volume.status
+ attached = volume.attach_data.status
+ LOG.debug("Volume %s is in status: %s, attach_status: %s",
+ volume.id, status, attached)
+ # Nova reports 'in-use' on 'attaching' volumes because we
+ # have a single volume status, and EC2 has 2. Ensure that
+ # if we aren't attached yet we return something other than
+ # 'in-use'
+ if status == 'in-use' and attached != 'attached':
+ return 'attaching'
+ else:
+ return status
- self.assertVolumeStatusWait(_volume_state, "in-use")
wait.re_search_wait(_volume_state, "in-use")
# NOTE(afazekas): Different Hypervisor backends names
@@ -296,6 +311,7 @@
def _part_state():
current = ssh.get_partitions().split('\n')
+ LOG.debug("Partition map for instance: %s", current)
if current > part_lines:
return 'INCREASE'
if current < part_lines:
@@ -311,7 +327,6 @@
self.assertVolumeStatusWait(_volume_state, "available")
wait.re_search_wait(_volume_state, "available")
- LOG.info("Volume %s state: %s", volume.id, volume.status)
wait.state_wait(_part_state, 'DECREASE')
@@ -323,7 +338,7 @@
self.assertAddressReleasedWait(address)
self.cancelResourceCleanUp(rcuk_a)
- LOG.info("state: %s", instance.state)
+ LOG.debug("Instance %s state: %s", instance.id, instance.state)
if instance.state != "stopped":
self.assertInstanceStateWait(instance, "stopped")
# TODO(afazekas): move steps from teardown to the test case