Merge "VPNaaS API Tests Enhancements"
diff --git a/tempest/api/baremetal/test_nodes.py b/tempest/api/baremetal/test_nodes.py
index b6432ad..1572840 100644
--- a/tempest/api/baremetal/test_nodes.py
+++ b/tempest/api/baremetal/test_nodes.py
@@ -87,3 +87,11 @@
resp, node = self.client.show_node(node['uuid'])
self.assertEqual('200', resp['status'])
self._assertExpected(new_p, node['properties'])
+
+ @test.attr(type='smoke')
+ def test_validate_driver_interface(self):
+ resp, body = self.client.validate_driver_interface(self.node['uuid'])
+ self.assertEqual('200', resp['status'])
+ core_interfaces = ['power', 'deploy']
+ for interface in core_interfaces:
+ self.assertIn(interface, body)
diff --git a/tempest/api/compute/admin/test_servers_negative.py b/tempest/api/compute/admin/test_servers_negative.py
index 15025ba..cccaf13 100644
--- a/tempest/api/compute/admin/test_servers_negative.py
+++ b/tempest/api/compute/admin/test_servers_negative.py
@@ -55,6 +55,8 @@
return flavor_id
@test.skip_because(bug="1298131")
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type=['negative', 'gate'])
def test_resize_server_using_overlimit_ram(self):
flavor_name = data_utils.rand_name("flavor-")
@@ -74,6 +76,8 @@
flavor_ref['id'])
@test.skip_because(bug="1298131")
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type=['negative', 'gate'])
def test_resize_server_using_overlimit_vcpus(self):
flavor_name = data_utils.rand_name("flavor-")
diff --git a/tempest/api/compute/flavors/test_flavors.py b/tempest/api/compute/flavors/test_flavors.py
index 0e6b9d6..c1c2d05 100644
--- a/tempest/api/compute/flavors/test_flavors.py
+++ b/tempest/api/compute/flavors/test_flavors.py
@@ -89,44 +89,40 @@
@test.attr(type='gate')
def test_list_flavors_detailed_filter_by_min_disk(self):
# The detailed list of flavors should be filtered by disk space
- resp, flavors = self.client.list_flavors_with_detail()
- flavors = sorted(flavors, key=lambda k: k['disk'])
- flavor_id = flavors[0]['id']
+ resp, flavor = self.client.get_flavor_details(self.flavor_ref)
+ flavor_id = flavor['id']
- params = {self._min_disk: flavors[0]['disk'] + 1}
+ params = {self._min_disk: flavor['disk'] + 1}
resp, flavors = self.client.list_flavors_with_detail(params)
self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
@test.attr(type='gate')
def test_list_flavors_detailed_filter_by_min_ram(self):
# The detailed list of flavors should be filtered by RAM
- resp, flavors = self.client.list_flavors_with_detail()
- flavors = sorted(flavors, key=lambda k: k['ram'])
- flavor_id = flavors[0]['id']
+ resp, flavor = self.client.get_flavor_details(self.flavor_ref)
+ flavor_id = flavor['id']
- params = {self._min_ram: flavors[0]['ram'] + 1}
+ params = {self._min_ram: flavor['ram'] + 1}
resp, flavors = self.client.list_flavors_with_detail(params)
self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
@test.attr(type='gate')
def test_list_flavors_filter_by_min_disk(self):
# The list of flavors should be filtered by disk space
- resp, flavors = self.client.list_flavors_with_detail()
- flavors = sorted(flavors, key=lambda k: k['disk'])
- flavor_id = flavors[0]['id']
+ resp, flavor = self.client.get_flavor_details(self.flavor_ref)
+ flavor_id = flavor['id']
- params = {self._min_disk: flavors[0]['disk'] + 1}
+ params = {self._min_disk: flavor['disk'] + 1}
resp, flavors = self.client.list_flavors(params)
self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
@test.attr(type='gate')
def test_list_flavors_filter_by_min_ram(self):
# The list of flavors should be filtered by RAM
- resp, flavors = self.client.list_flavors_with_detail()
- flavors = sorted(flavors, key=lambda k: k['ram'])
- flavor_id = flavors[0]['id']
+ resp, flavor = self.client.get_flavor_details(self.flavor_ref)
+ flavor_id = flavor['id']
- params = {self._min_ram: flavors[0]['ram'] + 1}
+ params = {self._min_ram: flavor['ram'] + 1}
resp, flavors = self.client.list_flavors(params)
self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index 6343ead..d3297ce 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -106,6 +106,8 @@
self.assertRaises(exceptions.BadRequest,
self.create_test_server, accessIPv6=IPv6)
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type=['negative', 'gate'])
def test_resize_nonexistent_server(self):
# Resize a non-existent server
@@ -114,6 +116,8 @@
self.client.resize,
nonexistent_server, self.flavor_ref)
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type=['negative', 'gate'])
def test_resize_server_with_non_existent_flavor(self):
# Resize a server with non-existent flavor
@@ -121,6 +125,8 @@
self.assertRaises(exceptions.BadRequest, self.client.resize,
self.server_id, flavor_ref=nonexistent_flavor)
+ @testtools.skipUnless(CONF.compute_feature_enabled.resize,
+ 'Resize not available.')
@test.attr(type=['negative', 'gate'])
def test_resize_server_with_null_flavor(self):
# Resize a server with null flavor
diff --git a/tempest/api/compute/test_authorization.py b/tempest/api/compute/test_authorization.py
index 375ddf8..fb8ded3 100644
--- a/tempest/api/compute/test_authorization.py
+++ b/tempest/api/compute/test_authorization.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import StringIO
+
from tempest.api.compute import base
from tempest import clients
from tempest.common.utils import data_utils
@@ -27,9 +29,10 @@
class AuthorizationTestJSON(base.BaseV2ComputeTest):
-
@classmethod
def setUpClass(cls):
+ if not CONF.service_available.glance:
+ raise cls.skipException('Glance is not available.')
# No network resources required for this test
cls.set_network_resources()
super(AuthorizationTestJSON, cls).setUpClass()
@@ -38,6 +41,7 @@
raise cls.skipException(msg)
cls.client = cls.os.servers_client
cls.images_client = cls.os.images_client
+ cls.glance_client = cls.os.image_client
cls.keypairs_client = cls.os.keypairs_client
cls.security_client = cls.os.security_groups_client
@@ -57,9 +61,14 @@
resp, cls.server = cls.client.get_server(server['id'])
name = data_utils.rand_name('image')
- resp, body = cls.images_client.create_image(server['id'], name)
- image_id = data_utils.parse_image_id(resp['location'])
- cls.images_client.wait_for_image_status(image_id, 'ACTIVE')
+ resp, body = cls.glance_client.create_image(name=name,
+ container_format='bare',
+ disk_format='raw',
+ is_public=False)
+ image_id = body['id']
+ image_file = StringIO.StringIO(('*' * 1024))
+ resp, body = cls.glance_client.update_image(image_id, data=image_file)
+ cls.glance_client.wait_for_image_status(image_id, 'active')
resp, cls.image = cls.images_client.get_image(image_id)
cls.keypairname = data_utils.rand_name('keypair')
diff --git a/tempest/api/compute/v3/admin/test_flavors.py b/tempest/api/compute/v3/admin/test_flavors.py
index 8a4e3cf..09d76b8 100644
--- a/tempest/api/compute/v3/admin/test_flavors.py
+++ b/tempest/api/compute/v3/admin/test_flavors.py
@@ -294,7 +294,7 @@
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
new_flavor_id = data_utils.rand_int_id(start=1000)
- ram = " 1024 "
+ ram = "1024"
resp, flavor = self.client.create_flavor(flavor_name,
ram, self.vcpus,
self.disk,
diff --git a/tempest/api/compute/v3/admin/test_flavors_negative.py b/tempest/api/compute/v3/admin/test_flavors_negative.py
index 3f8a2da..6d3308e 100644
--- a/tempest/api/compute/v3/admin/test_flavors_negative.py
+++ b/tempest/api/compute/v3/admin/test_flavors_negative.py
@@ -57,7 +57,7 @@
resp, flavor = self.client.create_flavor(flavor_name,
self.ram,
self.vcpus, self.disk,
- '',
+ None,
ephemeral=self.ephemeral,
swap=self.swap,
rxtx=self.rxtx)
diff --git a/tempest/api/compute/v3/servers/test_instance_actions.py b/tempest/api/compute/v3/servers/test_instance_actions.py
index 399541b..64339b8 100644
--- a/tempest/api/compute/v3/servers/test_instance_actions.py
+++ b/tempest/api/compute/v3/servers/test_instance_actions.py
@@ -40,12 +40,10 @@
self.assertTrue(any([i for i in body if i['action'] == 'create']))
self.assertTrue(any([i for i in body if i['action'] == 'reboot']))
- @test.skip_because(bug="1206032")
@test.attr(type='gate')
- @test.skip_because(bug="1281915")
def test_get_server_action(self):
# Get the action details of the provided server
- request_id = self.resp['x-compute-request-id']
+ request_id = self.resp['x-openstack-request-id']
resp, body = self.client.get_server_action(self.server_id,
request_id)
self.assertEqual(200, resp.status)
diff --git a/tempest/api/data_processing/base.py b/tempest/api/data_processing/base.py
index 0d6773c..ab0e83a 100644
--- a/tempest/api/data_processing/base.py
+++ b/tempest/api/data_processing/base.py
@@ -29,8 +29,8 @@
if not CONF.service_available.sahara:
raise cls.skipException('Sahara support is required')
- os = cls.get_client_manager()
- cls.client = os.data_processing_client
+ cls.os = cls.get_client_manager()
+ cls.client = cls.os.data_processing_client
cls.flavor_ref = CONF.compute.flavor_ref
diff --git a/tempest/api/data_processing/test_job_binaries.py b/tempest/api/data_processing/test_job_binaries.py
new file mode 100644
index 0000000..689c1fe
--- /dev/null
+++ b/tempest/api/data_processing/test_job_binaries.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2014 Mirantis Inc.
+#
+# 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.data_processing import base as dp_base
+from tempest.common.utils import data_utils
+from tempest import test
+
+
+class JobBinaryTest(dp_base.BaseDataProcessingTest):
+ """Link to the API documentation is http://docs.openstack.org/developer/
+ sahara/restapi/rest_api_v1.1_EDP.html#job-binaries
+ """
+ @classmethod
+ @test.safe_setup
+ def setUpClass(cls):
+ super(JobBinaryTest, cls).setUpClass()
+ cls.swift_job_binary_with_extra = {
+ 'url': 'swift://sahara-container.sahara/example.jar',
+ 'description': 'Test job binary',
+ 'extra': {
+ 'user': cls.os.credentials.username,
+ 'password': cls.os.credentials.password
+ }
+ }
+ # Create extra cls.swift_job_binary variable to use for comparison to
+ # job binary response body because response body has no 'extra' field.
+ cls.swift_job_binary = cls.swift_job_binary_with_extra.copy()
+ del cls.swift_job_binary['extra']
+
+ name = data_utils.rand_name('sahara-internal-job-binary')
+ cls.job_binary_data = 'Some script may be data'
+ job_binary_internal = cls.create_job_binary_internal(
+ name, cls.job_binary_data)[1]
+ cls.internal_db_job_binary = {
+ 'url': 'internal-db://%s' % job_binary_internal['id'],
+ 'description': 'Test job binary',
+ }
+
+ def _create_job_binary(self, binary_body, binary_name=None):
+ """Creates Job Binary with optional name specified.
+
+ It creates a link to data (jar, pig files, etc.) and ensures response
+ status, job binary name and response body. Returns id and name of
+ created job binary. Data may not exist when using Swift
+ as data storage. In other cases data must exist in storage.
+ """
+ if not binary_name:
+ # generate random name if it's not specified
+ binary_name = data_utils.rand_name('sahara-job-binary')
+
+ # create job binary
+ resp, body = self.create_job_binary(binary_name, **binary_body)
+
+ # ensure that binary created successfully
+ self.assertEqual(202, resp.status)
+ self.assertEqual(binary_name, body['name'])
+ if 'swift' in binary_body['url']:
+ binary_body = self.swift_job_binary
+ self.assertDictContainsSubset(binary_body, body)
+
+ return body['id'], binary_name
+
+ @test.attr(type='smoke')
+ def test_swift_job_binary_create(self):
+ self._create_job_binary(self.swift_job_binary_with_extra)
+
+ @test.attr(type='smoke')
+ def test_swift_job_binary_list(self):
+ binary_info = self._create_job_binary(self.swift_job_binary_with_extra)
+
+ # check for job binary in list
+ resp, binaries = self.client.list_job_binaries()
+ self.assertEqual(200, resp.status)
+ binaries_info = [(binary['id'], binary['name']) for binary in binaries]
+ self.assertIn(binary_info, binaries_info)
+
+ @test.attr(type='smoke')
+ def test_swift_job_binary_get(self):
+ binary_id, binary_name = self._create_job_binary(
+ self.swift_job_binary_with_extra)
+
+ # check job binary fetch by id
+ resp, binary = self.client.get_job_binary(binary_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(binary_name, binary['name'])
+ self.assertDictContainsSubset(self.swift_job_binary, binary)
+
+ @test.attr(type='smoke')
+ def test_swift_job_binary_delete(self):
+ binary_id = self._create_job_binary(
+ self.swift_job_binary_with_extra)[0]
+
+ # delete the job binary by id
+ resp = self.client.delete_job_binary(binary_id)[0]
+ self.assertEqual(204, resp.status)
+
+ @test.attr(type='smoke')
+ def test_internal_db_job_binary_create(self):
+ self._create_job_binary(self.internal_db_job_binary)
+
+ @test.attr(type='smoke')
+ def test_internal_db_job_binary_list(self):
+ binary_info = self._create_job_binary(self.internal_db_job_binary)
+
+ # check for job binary in list
+ resp, binaries = self.client.list_job_binaries()
+ self.assertEqual(200, resp.status)
+ binaries_info = [(binary['id'], binary['name']) for binary in binaries]
+ self.assertIn(binary_info, binaries_info)
+
+ @test.attr(type='smoke')
+ def test_internal_db_job_binary_get(self):
+ binary_id, binary_name = self._create_job_binary(
+ self.internal_db_job_binary)
+
+ # check job binary fetch by id
+ resp, binary = self.client.get_job_binary(binary_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(binary_name, binary['name'])
+ self.assertDictContainsSubset(self.internal_db_job_binary, binary)
+
+ @test.attr(type='smoke')
+ def test_internal_db_job_binary_delete(self):
+ binary_id = self._create_job_binary(self.internal_db_job_binary)[0]
+
+ # delete the job binary by id
+ resp = self.client.delete_job_binary(binary_id)[0]
+ self.assertEqual(204, resp.status)
+
+ @test.attr(type='smoke')
+ def test_job_binary_get_data(self):
+ binary_id = self._create_job_binary(self.internal_db_job_binary)[0]
+
+ # get data of job binary by id
+ resp, data = self.client.get_job_binary_data(binary_id)
+ self.assertEqual(200, resp.status)
+ self.assertEqual(data, self.job_binary_data)
diff --git a/tempest/api/network/admin/test_quotas.py b/tempest/api/network/admin/test_quotas.py
index a307986..d1a8faf 100644
--- a/tempest/api/network/admin/test_quotas.py
+++ b/tempest/api/network/admin/test_quotas.py
@@ -85,3 +85,50 @@
self.assertEqual('200', resp['status'])
for q in non_default_quotas['quotas']:
self.assertNotEqual(tenant_id, q['tenant_id'])
+
+ @test.requires_ext(extension='lbaas', service='network')
+ @test.attr(type='gate')
+ def test_lbaas_quotas(self):
+ # Add a tenant to conduct the test
+ test_tenant = data_utils.rand_name('test_tenant_')
+ test_description = data_utils.rand_name('desc_')
+ _, tenant = self.identity_admin_client.create_tenant(
+ name=test_tenant,
+ description=test_description)
+ tenant_id = tenant['id']
+ self.addCleanup(self.identity_admin_client.delete_tenant, tenant_id)
+ # Change lbaas quotas for tenant
+ new_quotas = {'vip': 1, 'pool': 2,
+ 'member': 3, 'health_monitor': 4}
+
+ resp, quota_set = self.admin_client.update_quotas(tenant_id,
+ **new_quotas)
+ self.assertEqual('200', resp['status'])
+ self.addCleanup(self.admin_client.reset_quotas, tenant_id)
+ self.assertEqual(1, quota_set['vip'])
+ self.assertEqual(2, quota_set['pool'])
+ self.assertEqual(3, quota_set['member'])
+ self.assertEqual(4, quota_set['health_monitor'])
+ # Confirm our tenant is listed among tenants with non default quotas
+ resp, non_default_quotas = self.admin_client.list_quotas()
+ self.assertEqual('200', resp['status'])
+ found = False
+ for qs in non_default_quotas['quotas']:
+ if qs['tenant_id'] == tenant_id:
+ found = True
+ self.assertTrue(found)
+ # Confirm from APi quotas were changed as requested for tenant
+ resp, quota_set = self.admin_client.show_quotas(tenant_id)
+ quota_set = quota_set['quota']
+ self.assertEqual('200', resp['status'])
+ self.assertEqual(1, quota_set['vip'])
+ self.assertEqual(2, quota_set['pool'])
+ self.assertEqual(3, quota_set['member'])
+ self.assertEqual(4, quota_set['health_monitor'])
+ # Reset quotas to default and confirm
+ resp, body = self.admin_client.reset_quotas(tenant_id)
+ self.assertEqual('204', resp['status'])
+ resp, non_default_quotas = self.admin_client.list_quotas()
+ self.assertEqual('200', resp['status'])
+ for q in non_default_quotas['quotas']:
+ self.assertNotEqual(tenant_id, q['tenant_id'])
diff --git a/tempest/api/network/test_allowed_address_pair.py b/tempest/api/network/test_allowed_address_pair.py
index e0e26da..c897716 100644
--- a/tempest/api/network/test_allowed_address_pair.py
+++ b/tempest/api/network/test_allowed_address_pair.py
@@ -70,6 +70,27 @@
self.assertTrue(port, msg)
self._confirm_allowed_address_pair(port[0], self.ip_address)
+ @test.attr(type='smoke')
+ def test_update_port_with_address_pair(self):
+ # Create a port without allowed address pair
+ resp, body = self.client.create_port(network_id=self.network['id'])
+ self.assertEqual('201', resp['status'])
+ port_id = body['port']['id']
+ self.addCleanup(self.client.delete_port, port_id)
+
+ # Confirm port is created
+ resp, body = self.client.show_port(port_id)
+ self.assertEqual('200', resp['status'])
+
+ # Update allowed address pair attribute of port
+ allowed_address_pairs = [{'ip_address': self.ip_address,
+ 'mac_address': self.mac_address}]
+ resp, body = self.client.update_port(port_id,
+ allowed_address_pairs=allowed_address_pairs)
+ self.assertEqual('200', resp['status'])
+ newport = body['port']
+ self._confirm_allowed_address_pair(newport, self.ip_address)
+
def _confirm_allowed_address_pair(self, port, ip):
msg = 'Port allowed address pairs should not be empty'
self.assertTrue(port['allowed_address_pairs'], msg)
diff --git a/tempest/api_schema/compute/agents.py b/tempest/api_schema/compute/agents.py
index b9ad240..e5f3a8d 100644
--- a/tempest/api_schema/compute/agents.py
+++ b/tempest/api_schema/compute/agents.py
@@ -22,7 +22,7 @@
'items': {
'type': 'object',
'properties': {
- 'agent_id': {'type': ['integer', 'string']},
+ 'agent_id': {'type': 'integer'},
'hypervisor': {'type': 'string'},
'os': {'type': 'string'},
'architecture': {'type': 'string'},
diff --git a/tempest/api_schema/compute/keypairs.py b/tempest/api_schema/compute/keypairs.py
index b8f905f..2ae410c 100644
--- a/tempest/api_schema/compute/keypairs.py
+++ b/tempest/api_schema/compute/keypairs.py
@@ -49,10 +49,7 @@
'fingerprint': {'type': 'string'},
'name': {'type': 'string'},
'public_key': {'type': 'string'},
- # NOTE: Now the type of 'user_id' is integer, but here
- # allows 'string' also because we will be able to change
- # it to 'uuid' in the future.
- 'user_id': {'type': ['integer', 'string']},
+ 'user_id': {'type': 'string'},
'private_key': {'type': 'string'}
},
# When create keypair API is being called with 'Public key'
diff --git a/tempest/api_schema/compute/migrations.py b/tempest/api_schema/compute/migrations.py
index 6723869..6549272 100644
--- a/tempest/api_schema/compute/migrations.py
+++ b/tempest/api_schema/compute/migrations.py
@@ -22,10 +22,7 @@
'items': {
'type': 'object',
'properties': {
- # NOTE: Now the type of 'id' is integer, but here
- # allows 'string' also because we will be able to
- # change it to 'uuid' in the future.
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'integer'},
'status': {'type': 'string'},
'instance_uuid': {'type': 'string'},
'source_node': {'type': 'string'},
@@ -33,12 +30,8 @@
'dest_node': {'type': 'string'},
'dest_compute': {'type': 'string'},
'dest_host': {'type': 'string'},
- 'old_instance_type_id': {
- 'type': ['integer', 'string']
- },
- 'new_instance_type_id': {
- 'type': ['integer', 'string']
- },
+ 'old_instance_type_id': {'type': 'integer'},
+ 'new_instance_type_id': {'type': 'integer'},
'created_at': {'type': 'string'},
'updated_at': {'type': ['string', 'null']}
},
diff --git a/tempest/api_schema/compute/servers.py b/tempest/api_schema/compute/servers.py
index 14e9ce9..2519eb5 100644
--- a/tempest/api_schema/compute/servers.py
+++ b/tempest/api_schema/compute/servers.py
@@ -56,13 +56,13 @@
'server': {
'type': 'object',
'properties': {
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'string'},
'name': {'type': 'string'},
'status': {'type': 'string'},
'image': {
'type': 'object',
'properties': {
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'string'},
'links': parameter_types.links
},
'required': ['id', 'links']
@@ -70,7 +70,7 @@
'flavor': {
'type': 'object',
'properties': {
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'string'},
'links': parameter_types.links
},
'required': ['id', 'links']
diff --git a/tempest/api_schema/compute/services.py b/tempest/api_schema/compute/services.py
index 4c58013..eaba129 100644
--- a/tempest/api_schema/compute/services.py
+++ b/tempest/api_schema/compute/services.py
@@ -22,10 +22,7 @@
'items': {
'type': 'object',
'properties': {
- # NOTE: Now the type of 'id' is integer, but here
- # allows 'string' also because we will be able to
- # change it to 'uuid' in the future.
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'integer'},
'zone': {'type': 'string'},
'host': {'type': 'string'},
'state': {'type': 'string'},
diff --git a/tempest/api_schema/compute/v2/floating_ips.py b/tempest/api_schema/compute/v2/floating_ips.py
index 03e6aef..fb3667b 100644
--- a/tempest/api_schema/compute/v2/floating_ips.py
+++ b/tempest/api_schema/compute/v2/floating_ips.py
@@ -27,7 +27,7 @@
# able to change it to 'uuid' in the future.
'id': {'type': ['integer', 'string']},
'pool': {'type': ['string', 'null']},
- 'instance_id': {'type': ['integer', 'string', 'null']},
+ 'instance_id': {'type': ['string', 'null']},
'ip': {
'type': 'string',
'format': 'ip-address'
@@ -58,7 +58,7 @@
# 'uuid' in the future.
'id': {'type': ['integer', 'string']},
'pool': {'type': ['string', 'null']},
- 'instance_id': {'type': ['integer', 'string', 'null']},
+ 'instance_id': {'type': ['string', 'null']},
'ip': {
'type': 'string',
'format': 'ip-address'
@@ -117,3 +117,14 @@
'required': ['floating_ips_bulk_create']
}
}
+
+delete_floating_ips_bulk = {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'floating_ips_bulk_delete': {'type': 'string'}
+ },
+ 'required': ['floating_ips_bulk_delete']
+ }
+}
diff --git a/tempest/api_schema/compute/v2/images.py b/tempest/api_schema/compute/v2/images.py
index d121060..90737a2 100644
--- a/tempest/api_schema/compute/v2/images.py
+++ b/tempest/api_schema/compute/v2/images.py
@@ -30,10 +30,7 @@
'server': {
'type': 'object',
'properties': {
- # NOTE: Now the type of 'id' is integer, but here
- # allows 'string' also because we will be able to
- # change it to 'uuid' in the future.
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'string'},
'links': parameter_types.links
},
'required': ['id', 'links']
diff --git a/tempest/api_schema/compute/v2/keypairs.py b/tempest/api_schema/compute/v2/keypairs.py
index 9a025c3..32d8cca 100644
--- a/tempest/api_schema/compute/v2/keypairs.py
+++ b/tempest/api_schema/compute/v2/keypairs.py
@@ -25,10 +25,7 @@
'public_key': {'type': 'string'},
'name': {'type': 'string'},
'fingerprint': {'type': 'string'},
- # NOTE: Now the type of 'user_id' is integer, but here
- # allows 'string' also because we will be able to change
- # it to 'uuid' in the future.
- 'user_id': {'type': ['integer', 'string']},
+ 'user_id': {'type': 'string'},
'deleted': {'type': 'boolean'},
'created_at': {'type': 'string'},
'updated_at': {'type': ['string', 'null']},
diff --git a/tempest/api_schema/compute/v2/servers.py b/tempest/api_schema/compute/v2/servers.py
index fe53abd..5e9fbd5 100644
--- a/tempest/api_schema/compute/v2/servers.py
+++ b/tempest/api_schema/compute/v2/servers.py
@@ -25,10 +25,7 @@
'server': {
'type': 'object',
'properties': {
- # NOTE: Now the type of 'id' is uuid, but here allows
- # 'integer' also because old OpenStack uses 'integer'
- # as a server id.
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'string'},
'security_groups': {'type': 'array'},
'links': parameter_types.links,
'adminPass': {'type': 'string'},
diff --git a/tempest/api_schema/compute/v2/volumes.py b/tempest/api_schema/compute/v2/volumes.py
index 84a659c..1af951f 100644
--- a/tempest/api_schema/compute/v2/volumes.py
+++ b/tempest/api_schema/compute/v2/volumes.py
@@ -38,7 +38,7 @@
'id': {'type': 'string'},
'device': {'type': 'string'},
'volumeId': {'type': 'string'},
- 'serverId': {'type': ['integer', 'string']}
+ 'serverId': {'type': 'string'}
}
# NOTE- If volume is not attached to any server
# then, 'attachments' attributes comes as array
@@ -86,7 +86,7 @@
'id': {'type': 'string'},
'device': {'type': 'string'},
'volumeId': {'type': 'string'},
- 'serverId': {'type': ['integer', 'string']}
+ 'serverId': {'type': 'string'}
}
# NOTE- If volume is not attached to any server
# then, 'attachments' attributes comes as array
diff --git a/tempest/api_schema/compute/v3/servers.py b/tempest/api_schema/compute/v3/servers.py
index 4fb2d87..7572029 100644
--- a/tempest/api_schema/compute/v3/servers.py
+++ b/tempest/api_schema/compute/v3/servers.py
@@ -25,10 +25,7 @@
'server': {
'type': 'object',
'properties': {
- # NOTE: Now the type of 'id' is uuid, but here allows
- # 'integer' also because old OpenStack uses 'integer'
- # as a server id.
- 'id': {'type': ['integer', 'string']},
+ 'id': {'type': 'string'},
'os-security-groups:security_groups': {'type': 'array'},
'links': parameter_types.links,
'admin_password': {'type': 'string'},
diff --git a/tempest/cli/simple_read_only/test_cinder.py b/tempest/cli/simple_read_only/test_cinder.py
index e9a0cee..63ad0e3 100644
--- a/tempest/cli/simple_read_only/test_cinder.py
+++ b/tempest/cli/simple_read_only/test_cinder.py
@@ -96,7 +96,6 @@
self.cinder('type-list')
def test_cinder_list_extensions(self):
- self.cinder('list-extensions')
roles = self.parser.listing(self.cinder('list-extensions'))
self.assertTableStruct(roles, ['Name', 'Summary', 'Alias', 'Updated'])
diff --git a/tempest/cli/simple_read_only/test_neutron.py b/tempest/cli/simple_read_only/test_neutron.py
index c1d58b5..49d079e 100644
--- a/tempest/cli/simple_read_only/test_neutron.py
+++ b/tempest/cli/simple_read_only/test_neutron.py
@@ -49,7 +49,8 @@
@test.attr(type='smoke')
def test_neutron_net_list(self):
- self.neutron('net-list')
+ net_list = self.parser.listing(self.neutron('net-list'))
+ self.assertTableStruct(net_list, ['id', 'name', 'subnets'])
@test.attr(type='smoke')
def test_neutron_ext_list(self):
@@ -111,11 +112,14 @@
@test.attr(type='smoke')
@test.requires_ext(extension='external-net', service='network')
def test_neutron_net_external_list(self):
- self.neutron('net-external-list')
+ net_ext_list = self.parser.listing(self.neutron('net-external-list'))
+ self.assertTableStruct(net_ext_list, ['id', 'name', 'subnets'])
@test.attr(type='smoke')
def test_neutron_port_list(self):
- self.neutron('port-list')
+ port_list = self.parser.listing(self.neutron('port-list'))
+ self.assertTableStruct(port_list, ['id', 'name', 'mac_address',
+ 'fixed_ips'])
@test.attr(type='smoke')
@test.requires_ext(extension='quotas', service='network')
@@ -125,7 +129,9 @@
@test.attr(type='smoke')
@test.requires_ext(extension='router', service='network')
def test_neutron_router_list(self):
- self.neutron('router-list')
+ router_list = self.parser.listing(self.neutron('router-list'))
+ self.assertTableStruct(router_list, ['id', 'name',
+ 'external_gateway_info'])
@test.attr(type='smoke')
@test.requires_ext(extension='security-group', service='network')
@@ -136,11 +142,18 @@
@test.attr(type='smoke')
@test.requires_ext(extension='security-group', service='network')
def test_neutron_security_group_rule_list(self):
- self.neutron('security-group-rule-list')
+ security_grp = self.parser.listing(self.neutron
+ ('security-group-rule-list'))
+ self.assertTableStruct(security_grp, ['id', 'security_group',
+ 'direction', 'protocol',
+ 'remote_ip_prefix',
+ 'remote_group'])
@test.attr(type='smoke')
def test_neutron_subnet_list(self):
- self.neutron('subnet-list')
+ subnet_list = self.parser.listing(self.neutron('subnet-list'))
+ self.assertTableStruct(subnet_list, ['id', 'name', 'cidr',
+ 'allocation_pools'])
@test.attr(type='smoke')
def test_neutron_help(self):
diff --git a/tempest/common/commands.py b/tempest/common/commands.py
index 6720847..2ab008d 100644
--- a/tempest/common/commands.py
+++ b/tempest/common/commands.py
@@ -50,7 +50,7 @@
def iptables_raw(table):
- return sudo_cmd_call("iptables -v -S -t " + table)
+ return sudo_cmd_call("iptables --line-numbers -L -nv -t " + table)
def ip_ns_list():
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index 4e35aaa..93329bc 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -105,9 +105,24 @@
"T107: service tag should not be in path")
+def no_official_client_manager_in_api_tests(physical_line, filename):
+ """Check that the OfficialClientManager isn't used in the api tests
+
+ The api tests should not use the official clients.
+
+ T108: Can not use OfficialClientManager in the API tests
+ """
+ if 'tempest/api' in filename:
+ if 'OfficialClientManager' in physical_line:
+ return (physical_line.find('OfficialClientManager'),
+ 'T108: OfficialClientManager can not be used in the api '
+ 'tests')
+
+
def factory(register):
register(import_no_clients_in_api)
register(scenario_tests_need_service_tags)
register(no_setupclass_for_unit_tests)
register(no_vi_headers)
register(service_tags_not_in_module_path)
+ register(no_official_client_manager_in_api_tests)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 07d8828..e6593db 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -21,6 +21,7 @@
import subprocess
import time
+from cinderclient import exceptions as cinder_exceptions
from heatclient import exc as heat_exceptions
import netaddr
from neutronclient.common import exceptions as exc
@@ -354,7 +355,7 @@
return server
def create_volume(self, client=None, size=1, name=None,
- snapshot_id=None, imageRef=None):
+ snapshot_id=None, imageRef=None, volume_type=None):
if client is None:
client = self.volume_client
if name is None:
@@ -362,7 +363,8 @@
LOG.debug("Creating a volume (size: %s, name: %s)", size, name)
volume = client.volumes.create(size=size, display_name=name,
snapshot_id=snapshot_id,
- imageRef=imageRef)
+ imageRef=imageRef,
+ volume_type=volume_type)
self.set_resource(name, volume)
self.assertEqual(name, volume.display_name)
self.status_timeout(client.volumes, volume.id, 'available')
@@ -623,6 +625,84 @@
timeout=CONF.baremetal.unprovision_timeout)
+class EncryptionScenarioTest(OfficialClientTest):
+ """
+ Base class for encryption scenario tests
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(EncryptionScenarioTest, cls).setUpClass()
+
+ # use admin credentials to create encrypted volume types
+ admin_creds = cls.admin_credentials()
+ manager = clients.OfficialClientManager(credentials=admin_creds)
+ cls.admin_volume_client = manager.volume_client
+
+ def _wait_for_volume_status(self, status):
+ self.status_timeout(
+ self.volume_client.volumes, self.volume.id, status)
+
+ def _wait_for_volume_deletion(self):
+ self.delete_timeout(
+ self.volume_client.volumes, self.volume.id,
+ not_found_exception=cinder_exceptions.NotFound)
+
+ def nova_boot(self):
+ self.keypair = self.create_keypair()
+ create_kwargs = {'key_name': self.keypair.name}
+ self.server = self.create_server(self.compute_client,
+ image=self.image,
+ create_kwargs=create_kwargs)
+
+ def create_volume_type(self, client=None, name=None):
+ if not client:
+ client = self.admin_volume_client
+ if not name:
+ name = 'generic'
+ randomized_name = data_utils.rand_name('scenario-type-' + name + '-')
+ LOG.debug("Creating a volume type: %s", randomized_name)
+ volume_type = client.volume_types.create(randomized_name)
+ self.addCleanup(client.volume_types.delete, volume_type.id)
+ return volume_type
+
+ def create_encryption_type(self, client=None, type_id=None, provider=None,
+ key_size=None, cipher=None,
+ control_location=None):
+ if not client:
+ client = self.admin_volume_client
+ if not type_id:
+ volume_type = self.create_volume_type()
+ type_id = volume_type.id
+ LOG.debug("Creating an encryption type for volume type: %s", type_id)
+ client.volume_encryption_types.create(type_id,
+ {'provider': provider,
+ 'key_size': key_size,
+ 'cipher': cipher,
+ 'control_location':
+ control_location})
+
+ def nova_volume_attach(self):
+ attach_volume_client = self.compute_client.volumes.create_server_volume
+ volume = attach_volume_client(self.server.id,
+ self.volume.id,
+ '/dev/vdb')
+ self.assertEqual(self.volume.id, volume.id)
+ self._wait_for_volume_status('in-use')
+
+ def nova_volume_detach(self):
+ detach_volume_client = self.compute_client.volumes.delete_server_volume
+ detach_volume_client(self.server.id, self.volume.id)
+ self._wait_for_volume_status('available')
+
+ volume = self.volume_client.volumes.get(self.volume.id)
+ self.assertEqual('available', volume.status)
+
+ def cinder_delete_encrypted(self):
+ self.volume_client.volumes.delete(self.volume.id)
+ self._wait_for_volume_deletion()
+
+
class NetworkScenarioTest(OfficialClientTest):
"""
Base class for network scenario tests
diff --git a/tempest/scenario/test_encrypted_cinder_volumes.py b/tempest/scenario/test_encrypted_cinder_volumes.py
new file mode 100644
index 0000000..f223cbf
--- /dev/null
+++ b/tempest/scenario/test_encrypted_cinder_volumes.py
@@ -0,0 +1,69 @@
+# Copyright (c) 2014 The Johns Hopkins University/Applied Physics Laboratory
+# 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.scenario import manager
+from tempest import test
+
+
+class TestEncryptedCinderVolumes(manager.EncryptionScenarioTest):
+
+ """
+ This test is for verifying the functionality of encrypted cinder volumes.
+
+ For both LUKS and cryptsetup encryption types, this test performs
+ the following:
+ * Creates an image in Glance
+ * Boots an instance from the image
+ * Creates an encryption type (as admin)
+ * Creates a volume of that encryption type (as a regular user)
+ * Attaches and detaches the encrypted volume to the instance
+ * Deletes the encrypted volume
+ """
+
+ def launch_instance(self):
+ self.glance_image_create()
+ self.nova_boot()
+
+ def create_encrypted_volume(self, encryption_provider):
+ volume_type = self.create_volume_type(name='luks')
+ self.create_encryption_type(type_id=volume_type.id,
+ provider=encryption_provider,
+ key_size=512,
+ cipher='aes-xts-plain64',
+ control_location='front-end')
+ self.volume = self.create_volume(volume_type=volume_type.name)
+
+ def attach_detach_volume(self):
+ self.nova_volume_attach()
+ self.nova_volume_detach()
+
+ def delete_volume(self):
+ self.cinder_delete_encrypted()
+
+ @test.services('compute', 'volume', 'image')
+ def test_encrypted_cinder_volumes_luks(self):
+ self.launch_instance()
+ self.create_encrypted_volume('nova.volume.encryptors.'
+ 'luks.LuksEncryptor')
+ self.attach_detach_volume()
+ self.delete_volume()
+
+ @test.services('compute', 'volume', 'image')
+ def test_encrypted_cinder_volumes_cryptsetup(self):
+ self.launch_instance()
+ self.create_encrypted_volume('nova.volume.encryptors.'
+ 'cryptsetup.CryptsetupEncryptor')
+ self.attach_detach_volume()
+ self.delete_volume()
diff --git a/tempest/services/baremetal/base.py b/tempest/services/baremetal/base.py
index 321b08b..f98ecff 100644
--- a/tempest/services/baremetal/base.py
+++ b/tempest/services/baremetal/base.py
@@ -122,7 +122,7 @@
return resp, self.deserialize(body)
- def _show_request(self, resource, uuid, permanent=False):
+ def _show_request(self, resource, uuid, permanent=False, **kwargs):
"""
Gets a specific object of the specified type.
@@ -130,7 +130,10 @@
:return: Serialized object as a dictionary.
"""
- uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
+ if 'uri' in kwargs:
+ uri = kwargs['uri']
+ else:
+ uri = self._get_uri(resource, uuid=uuid, permanent=permanent)
resp, body = self.get(uri)
return resp, self.deserialize(body)
diff --git a/tempest/services/baremetal/v1/base_v1.py b/tempest/services/baremetal/v1/base_v1.py
index ea0ccc9..61342eb 100644
--- a/tempest/services/baremetal/v1/base_v1.py
+++ b/tempest/services/baremetal/v1/base_v1.py
@@ -239,3 +239,19 @@
target = {'target': state}
return self._put_request('nodes/%s/states/power' % node_uuid,
target)
+
+ @base.handle_errors
+ def validate_driver_interface(self, node_uuid):
+ """
+ Get all driver interfaces of a specific node.
+
+ :param uuid: Unique identifier of the node in UUID format.
+
+ """
+
+ uri = '{pref}/{res}/{uuid}/{postf}'.format(pref=self.uri_prefix,
+ res='nodes',
+ uuid=node_uuid,
+ postf='validate')
+
+ return self._show_request('nodes', node_uuid, uri=uri)
diff --git a/tempest/services/botoclients.py b/tempest/services/botoclients.py
index 7616a99..7af904b 100644
--- a/tempest/services/botoclients.py
+++ b/tempest/services/botoclients.py
@@ -37,13 +37,16 @@
*args, **kwargs):
# FIXME(andreaf) replace credentials and auth_url with auth_provider
+ insecure_ssl = CONF.identity.disable_ssl_certificate_validation
+
self.connection_timeout = str(CONF.boto.http_socket_timeout)
self.num_retries = str(CONF.boto.num_retries)
self.build_timeout = CONF.boto.build_timeout
self.ks_cred = {"username": username,
"password": password,
"auth_url": auth_url,
- "tenant_name": tenant_name}
+ "tenant_name": tenant_name,
+ "insecure": insecure_ssl}
def _keystone_aws_get(self):
# FIXME(andreaf) Move EC2 credentials to AuthProvider
@@ -90,7 +93,10 @@
self._config_boto_timeout(self.connection_timeout, self.num_retries)
if not all((self.connection_data["aws_access_key_id"],
self.connection_data["aws_secret_access_key"])):
- if all(self.ks_cred.itervalues()):
+ if all([self.ks_cred.get('auth_url'),
+ self.ks_cred.get('username'),
+ self.ks_cred.get('tenant_name'),
+ self.ks_cred.get('password')]):
ec2_cred = self._keystone_aws_get()
self.connection_data["aws_access_key_id"] = \
ec2_cred.access
@@ -109,6 +115,7 @@
def __init__(self, *args, **kwargs):
super(APIClientEC2, self).__init__(*args, **kwargs)
+ insecure_ssl = CONF.identity.disable_ssl_certificate_validation
aws_access = CONF.boto.aws_access
aws_secret = CONF.boto.aws_secret
purl = urlparse.urlparse(CONF.boto.ec2_url)
@@ -129,6 +136,7 @@
self.connection_data = {"aws_access_key_id": aws_access,
"aws_secret_access_key": aws_secret,
"is_secure": purl.scheme == "https",
+ "validate_certs": not insecure_ssl,
"region": region,
"host": purl.hostname,
"port": port,
@@ -187,6 +195,7 @@
def __init__(self, *args, **kwargs):
super(ObjectClientS3, self).__init__(*args, **kwargs)
+ insecure_ssl = CONF.identity.disable_ssl_certificate_validation
aws_access = CONF.boto.aws_access
aws_secret = CONF.boto.aws_secret
purl = urlparse.urlparse(CONF.boto.s3_url)
@@ -201,6 +210,7 @@
self.connection_data = {"aws_access_key_id": aws_access,
"aws_secret_access_key": aws_secret,
"is_secure": purl.scheme == "https",
+ "validate_certs": not insecure_ssl,
"host": purl.hostname,
"port": port,
"calling_format": boto.s3.connection.
diff --git a/tempest/services/compute/json/floating_ips_client.py b/tempest/services/compute/json/floating_ips_client.py
index 92b4ddf..0028eea 100644
--- a/tempest/services/compute/json/floating_ips_client.py
+++ b/tempest/services/compute/json/floating_ips_client.py
@@ -137,4 +137,5 @@
post_body = json.dumps({'ip_range': ip_range})
resp, body = self.put('os-floating-ips-bulk/delete', post_body)
body = json.loads(body)
+ self.validate_response(schema.delete_floating_ips_bulk, resp, body)
return resp, body['floating_ips_bulk_delete']
diff --git a/tempest/tests/test_commands.py b/tempest/tests/test_commands.py
index bdb9269..1e2925b 100644
--- a/tempest/tests/test_commands.py
+++ b/tempest/tests/test_commands.py
@@ -47,7 +47,8 @@
@mock.patch('subprocess.Popen')
def test_iptables_raw(self, mock):
table = 'filter'
- expected = ['/usr/bin/sudo', '-n', 'iptables', '-v', '-S', '-t',
+ expected = ['/usr/bin/sudo', '-n', 'iptables', '--line-numbers',
+ '-L', '-nv', '-t',
'%s' % table]
commands.iptables_raw(table)
mock.assert_called_once_with(expected, **self.subprocess_args)
diff --git a/tempest/tests/test_hacking.py b/tempest/tests/test_hacking.py
index 91c3274..52fdf7e 100644
--- a/tempest/tests/test_hacking.py
+++ b/tempest/tests/test_hacking.py
@@ -99,3 +99,11 @@
'./tempest/scenario/compute/fake_test.py'))
self.assertFalse(checks.service_tags_not_in_module_path(
"@test.services('compute')", './tempest/api/image/fake_test.py'))
+
+ def test_no_official_client_manager_in_api_tests(self):
+ self.assertTrue(checks.no_official_client_manager_in_api_tests(
+ "cls.official_client = clients.OfficialClientManager(credentials)",
+ "tempest/api/compute/base.py"))
+ self.assertFalse(checks.no_official_client_manager_in_api_tests(
+ "cls.official_client = clients.OfficialClientManager(credentials)",
+ "tempest/scenario/fake_test.py"))
diff --git a/tempest/tests/test_ssh.py b/tempest/tests/test_ssh.py
index bf1f553..27cd6b5 100644
--- a/tempest/tests/test_ssh.py
+++ b/tempest/tests/test_ssh.py
@@ -36,13 +36,13 @@
rsa_mock.assert_called_once_with(mock.sentinel.csio)
cs_mock.assert_called_once_with('mykey')
rsa_mock.reset_mock()
- cs_mock.rest_mock()
+ cs_mock.reset_mock()
pkey = mock.sentinel.pkey
# Shouldn't call out to load a file from RSAKey, since
# a sentinel isn't a basestring...
ssh.Client('localhost', 'root', pkey=pkey)
- rsa_mock.assert_not_called()
- cs_mock.assert_not_called()
+ self.assertEqual(0, rsa_mock.call_count)
+ self.assertEqual(0, cs_mock.call_count)
def _set_ssh_connection_mocks(self):
client_mock = mock.MagicMock()
@@ -75,7 +75,7 @@
password=None
)]
self.assertEqual(expected_connect, client_mock.connect.mock_calls)
- s_mock.assert_not_called()
+ self.assertEqual(0, s_mock.call_count)
def test_get_ssh_connection_two_attemps(self):
c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()