Merge "Verify flavor extra specs attributes of Nova APIs"
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/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/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 c377c30..ab1ccc4 100644
--- a/tempest/api/compute/v3/servers/test_server_actions.py
+++ b/tempest/api/compute/v3/servers/test_server_actions.py
@@ -329,6 +329,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/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/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/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/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/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/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/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/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/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()