Merge "Skip ceilometer test test_check_glance_v*_notifications"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index f80fc1b..08f3fd4 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -411,6 +411,10 @@
# Does the test environment support pausing? (boolean value)
#pause=true
+# Does the test environment support shelving/unshelving?
+# (boolean value)
+#shelve=true
+
# Does the test environment support suspend/resume? (boolean
# value)
#suspend=true
diff --git a/tempest/api/compute/servers/test_delete_server.py b/tempest/api/compute/servers/test_delete_server.py
index 9e34922..9c8271f 100644
--- a/tempest/api/compute/servers/test_delete_server.py
+++ b/tempest/api/compute/servers/test_delete_server.py
@@ -70,6 +70,8 @@
self.assertEqual('204', resp['status'])
self.client.wait_for_server_termination(server['id'])
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type='gate')
def test_delete_server_while_in_shelved_state(self):
# Delete a server while it's VM state is Shelved
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 71fcbff..ee525e7 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -403,6 +403,8 @@
self.assertEqual(202, resp.status)
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type='gate')
def test_shelve_unshelve_server(self):
resp, server = self.client.shelve_server(self.server_id)
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index d3297ce..792b523 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -425,6 +425,8 @@
self.client.restore_soft_deleted_server,
self.server_id)
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type=['negative', 'gate'])
def test_shelve_non_existent_server(self):
# shelve a non existent server
@@ -432,6 +434,8 @@
self.assertRaises(exceptions.NotFound, self.client.shelve_server,
nonexistent_server)
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type=['negative', 'gate'])
def test_shelve_shelved_server(self):
# shelve a shelved server.
@@ -460,6 +464,8 @@
self.client.unshelve_server(self.server_id)
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type=['negative', 'gate'])
def test_unshelve_non_existent_server(self):
# unshelve a non existent server
@@ -467,6 +473,8 @@
self.assertRaises(exceptions.NotFound, self.client.unshelve_server,
nonexistent_server)
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type=['negative', 'gate'])
def test_unshelve_server_invalid_state(self):
# unshelve an active server.
diff --git a/tempest/api/compute/v3/servers/test_delete_server.py b/tempest/api/compute/v3/servers/test_delete_server.py
index add69ab..e2b47ee 100644
--- a/tempest/api/compute/v3/servers/test_delete_server.py
+++ b/tempest/api/compute/v3/servers/test_delete_server.py
@@ -68,6 +68,8 @@
self.assertEqual('204', resp['status'])
self.client.wait_for_server_termination(server['id'])
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type='gate')
def test_delete_server_while_in_shelved_state(self):
# Delete a server while it's VM state is Shelved
diff --git a/tempest/api/compute/v3/servers/test_server_actions.py b/tempest/api/compute/v3/servers/test_server_actions.py
index 3ee8050..4404043 100644
--- a/tempest/api/compute/v3/servers/test_server_actions.py
+++ b/tempest/api/compute/v3/servers/test_server_actions.py
@@ -394,6 +394,8 @@
self.assertEqual(202, resp.status)
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type='gate')
def test_shelve_unshelve_server(self):
resp, server = self.client.shelve_server(self.server_id)
diff --git a/tempest/api/compute/v3/servers/test_servers_negative.py b/tempest/api/compute/v3/servers/test_servers_negative.py
index 90deaa9..f8ff7c8 100644
--- a/tempest/api/compute/v3/servers/test_servers_negative.py
+++ b/tempest/api/compute/v3/servers/test_servers_negative.py
@@ -397,6 +397,8 @@
self.client.restore_soft_deleted_server,
self.server_id)
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type=['negative', 'gate'])
def test_shelve_non_existent_server(self):
# shelve a non existent server
@@ -404,6 +406,8 @@
self.assertRaises(exceptions.NotFound, self.client.shelve_server,
nonexistent_server)
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type=['negative', 'gate'])
def test_shelve_shelved_server(self):
# shelve a shelved server.
@@ -431,6 +435,8 @@
self.client.unshelve_server(self.server_id)
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type=['negative', 'gate'])
def test_unshelve_non_existent_server(self):
# unshelve a non existent server
@@ -438,6 +444,8 @@
self.assertRaises(exceptions.NotFound, self.client.unshelve_server,
nonexistent_server)
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
@test.attr(type=['negative', 'gate'])
def test_unshelve_server_invalid_state(self):
# unshelve an active server.
diff --git a/tempest/api/data_processing/base.py b/tempest/api/data_processing/base.py
index cfb5a3d..65085b9 100644
--- a/tempest/api/data_processing/base.py
+++ b/tempest/api/data_processing/base.py
@@ -40,6 +40,7 @@
cls._data_sources = []
cls._job_binary_internals = []
cls._job_binaries = []
+ cls._jobs = []
@classmethod
def tearDownClass(cls):
@@ -47,12 +48,13 @@
cls.client.delete_cluster_template)
cls.cleanup_resources(getattr(cls, '_node_group_templates', []),
cls.client.delete_node_group_template)
- cls.cleanup_resources(getattr(cls, '_data_sources', []),
- cls.client.delete_data_source)
- cls.cleanup_resources(getattr(cls, '_job_binary_internals', []),
- cls.client.delete_job_binary_internal)
+ cls.cleanup_resources(getattr(cls, '_jobs', []), cls.client.delete_job)
cls.cleanup_resources(getattr(cls, '_job_binaries', []),
cls.client.delete_job_binary)
+ cls.cleanup_resources(getattr(cls, '_job_binary_internals', []),
+ cls.client.delete_job_binary_internal)
+ cls.cleanup_resources(getattr(cls, '_data_sources', []),
+ cls.client.delete_data_source)
cls.clear_isolated_creds()
super(BaseDataProcessingTest, cls).tearDownClass()
@@ -132,6 +134,7 @@
return resp_body
+ @classmethod
def create_job_binary(cls, name, url, extra=None, **kwargs):
"""Creates watched job binary with specified params.
@@ -144,3 +147,18 @@
cls._job_binaries.append(resp_body['id'])
return resp_body
+
+ @classmethod
+ def create_job(cls, name, job_type, mains, libs=None, **kwargs):
+ """Creates watched job with specified params.
+
+ It supports passing additional params using kwargs and returns created
+ object. All resources created in this method will be automatically
+ removed in tearDownClass method.
+ """
+ _, resp_body = cls.client.create_job(name,
+ job_type, mains, libs, **kwargs)
+ # store id of created job
+ cls._jobs.append(resp_body['id'])
+
+ return resp_body
diff --git a/tempest/api/data_processing/test_jobs.py b/tempest/api/data_processing/test_jobs.py
new file mode 100644
index 0000000..8591dbd
--- /dev/null
+++ b/tempest/api/data_processing/test_jobs.py
@@ -0,0 +1,90 @@
+# 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 JobTest(dp_base.BaseDataProcessingTest):
+ """Link to the API documentation is http://docs.openstack.org/developer/
+ sahara/restapi/rest_api_v1.1_EDP.html#jobs
+ """
+ @classmethod
+ @test.safe_setup
+ def setUpClass(cls):
+ super(JobTest, cls).setUpClass()
+ # create job binary
+ job_binary = {
+ 'name': data_utils.rand_name('sahara-job-binary'),
+ 'url': 'swift://sahara-container.sahara/example.jar',
+ 'description': 'Test job binary',
+ 'extra': {
+ 'user': cls.os.credentials.username,
+ 'password': cls.os.credentials.password
+ }
+ }
+ resp_body = cls.create_job_binary(**job_binary)
+ job_binary_id = resp_body['id']
+
+ cls.job = {
+ 'job_type': 'Pig',
+ 'mains': [job_binary_id]
+ }
+
+ def _create_job(self, job_name=None):
+ """Creates Job with optional name specified.
+
+ It creates job and ensures job name. Returns id and name of created
+ job.
+ """
+ if not job_name:
+ # generate random name if it's not specified
+ job_name = data_utils.rand_name('sahara-job')
+
+ # create job
+ resp_body = self.create_job(job_name, **self.job)
+
+ # ensure that job created successfully
+ self.assertEqual(job_name, resp_body['name'])
+
+ return resp_body['id'], job_name
+
+ @test.attr(type='smoke')
+ def test_job_create(self):
+ self._create_job()
+
+ @test.attr(type='smoke')
+ def test_job_list(self):
+ job_info = self._create_job()
+
+ # check for job in list
+ _, jobs = self.client.list_jobs()
+ jobs_info = [(job['id'], job['name']) for job in jobs]
+ self.assertIn(job_info, jobs_info)
+
+ @test.attr(type='smoke')
+ def test_job_get(self):
+ job_id, job_name = self._create_job()
+
+ # check job fetch by id
+ _, job = self.client.get_job(job_id)
+ self.assertEqual(job_name, job['name'])
+
+ @test.attr(type='smoke')
+ def test_job_delete(self):
+ job_id, _ = self._create_job()
+
+ # delete the job by id
+ self.client.delete_job(job_id)
diff --git a/tempest/api/identity/admin/v3/test_list_projects.py b/tempest/api/identity/admin/v3/test_list_projects.py
new file mode 100644
index 0000000..a3944e2
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_list_projects.py
@@ -0,0 +1,73 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P
+# 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.identity import base
+from tempest.common.utils import data_utils
+from tempest import test
+
+
+class ListProjectsTestJSON(base.BaseIdentityV3AdminTest):
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(ListProjectsTestJSON, cls).setUpClass()
+ cls.project_ids = list()
+ cls.data.setup_test_domain()
+ # Create project with domain
+ cls.p1_name = data_utils.rand_name('project')
+ _, cls.p1 = cls.client.create_project(
+ cls.p1_name, enabled=False, domain_id=cls.data.domain['id'])
+ cls.data.projects.append(cls.p1)
+ cls.project_ids.append(cls.p1['id'])
+ # Create default project
+ p2_name = data_utils.rand_name('project')
+ _, cls.p2 = cls.client.create_project(p2_name)
+ cls.data.projects.append(cls.p2)
+ cls.project_ids.append(cls.p2['id'])
+
+ @test.attr(type='gate')
+ def test_projects_list(self):
+ # List projects
+ resp, list_projects = self.client.list_projects()
+
+ for p in self.project_ids:
+ _, get_project = self.client.get_project(p)
+ self.assertIn(get_project, list_projects)
+
+ @test.attr(type='gate')
+ def test_list_projects_with_domains(self):
+ # List projects with domain
+ self._list_projects_with_params(
+ {'domain_id': self.data.domain['id']}, 'domain_id')
+
+ @test.attr(type='gate')
+ def test_list_projects_with_enabled(self):
+ # List the projects with enabled
+ self._list_projects_with_params({'enabled': False}, 'enabled')
+
+ @test.attr(type='gate')
+ def test_list_projects_with_name(self):
+ # List projects with name
+ self._list_projects_with_params({'name': self.p1_name}, 'name')
+
+ def _list_projects_with_params(self, params, key):
+ resp, body = self.client.list_projects(params)
+ self.assertIn(self.p1[key], map(lambda x: x[key], body))
+ self.assertNotIn(self.p2[key], map(lambda x: x[key], body))
+
+
+class ListProjectsTestXML(ListProjectsTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index 77acd57..5890eab 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -13,35 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
-from six import moves
-
from tempest.api.identity import base
from tempest.common.utils import data_utils
-from tempest import exceptions
from tempest import test
class ProjectsTestJSON(base.BaseIdentityV3AdminTest):
_interface = 'json'
- def _delete_project(self, project_id):
- self.client.delete_project(project_id)
- self.assertRaises(
- exceptions.NotFound, self.client.get_project, project_id)
-
- @test.attr(type='gate')
- def test_project_list_delete(self):
- # Create several projects and delete them
- for _ in moves.xrange(3):
- _, project = self.client.create_project(
- data_utils.rand_name('project-new'))
- self.addCleanup(self._delete_project, project['id'])
-
- _, list_projects = self.client.list_projects()
-
- _, get_project = self.client.get_project(project['id'])
- self.assertIn(get_project, list_projects)
-
@test.attr(type='gate')
def test_project_create_with_description(self):
# Create project with a description
@@ -60,6 +39,21 @@
'to be set')
@test.attr(type='gate')
+ def test_project_create_with_domain(self):
+ # Create project with a domain
+ self.data.setup_test_domain()
+ project_name = data_utils.rand_name('project')
+ resp, project = self.client.create_project(
+ project_name, domain_id=self.data.domain['id'])
+ self.data.projects.append(project)
+ project_id = project['id']
+ self.assertEqual(project_name, project['name'])
+ self.assertEqual(self.data.domain['id'], project['domain_id'])
+ _, body = self.client.get_project(project_id)
+ self.assertEqual(project_name, body['name'])
+ self.assertEqual(self.data.domain['id'], body['domain_id'])
+
+ @test.attr(type='gate')
def test_project_create_enabled(self):
# Create a project that is enabled
project_name = data_utils.rand_name('project-')
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index 1ef9aa1..b21aa44 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -17,7 +17,7 @@
import hashlib
import random
import re
-from six import moves
+import six
import time
import zlib
@@ -54,15 +54,36 @@
object_name = data_utils.rand_name(name='LObject')
data = data_utils.arbitrary_string()
segments = 10
- data_segments = [data + str(i) for i in moves.xrange(segments)]
+ data_segments = [data + str(i) for i in six.moves.xrange(segments)]
# uploading segments
- for i in moves.xrange(segments):
+ for i in six.moves.xrange(segments):
resp, _ = self.object_client.create_object_segments(
self.container_name, object_name, i, data_segments[i])
self.assertEqual(resp['status'], '201')
return object_name, data_segments
+ def _copy_object_2d(self, src_object_name, metadata=None):
+ dst_object_name = data_utils.rand_name(name='TestObject')
+ resp, _ = self.object_client.copy_object_2d_way(self.container_name,
+ src_object_name,
+ dst_object_name,
+ metadata=metadata)
+ return dst_object_name, resp
+
+ def _check_copied_obj(self, dst_object_name, src_body,
+ in_meta=None, not_in_meta=None):
+ resp, dest_body = self.object_client.get_object(self.container_name,
+ dst_object_name)
+
+ self.assertEqual(src_body, dest_body)
+ if in_meta:
+ for meta_key in in_meta:
+ self.assertIn('x-object-meta-' + meta_key, resp)
+ if not_in_meta:
+ for meta_key in not_in_meta:
+ self.assertNotIn('x-object-meta-' + meta_key, resp)
+
@test.attr(type='gate')
def test_create_object(self):
# create object
@@ -765,10 +786,7 @@
# change the content type of an existing object
# create object
- object_name = data_utils.rand_name(name='TestObject')
- data = data_utils.arbitrary_string()
- self.object_client.create_object(self.container_name,
- object_name, data)
+ object_name, data = self._create_object()
# get the old content type
resp_tmp, _ = self.object_client.list_object_metadata(
self.container_name, object_name)
@@ -805,20 +823,12 @@
dst_object_name)
self.assertEqual(resp['status'], '201')
self.assertHeaders(resp, 'Object', 'COPY')
-
- self.assertIn('last-modified', resp)
- self.assertIn('x-copied-from', resp)
- self.assertIn('x-copied-from-last-modified', resp)
- self.assertNotEqual(len(resp['last-modified']), 0)
self.assertEqual(
resp['x-copied-from'],
self.container_name + "/" + src_object_name)
- self.assertNotEqual(len(resp['x-copied-from-last-modified']), 0)
# check data
- resp, body = self.object_client.get_object(self.container_name,
- dst_object_name)
- self.assertEqual(body, src_data)
+ self._check_copied_obj(dst_object_name, src_data)
@test.attr(type='smoke')
def test_copy_object_across_containers(self):
@@ -862,15 +872,82 @@
self.assertIn(actual_meta_key, resp)
self.assertEqual(resp[actual_meta_key], meta_value)
+ @test.attr(type='smoke')
+ def test_copy_object_with_x_fresh_metadata(self):
+ # create source object
+ metadata = {'x-object-meta-src': 'src_value'}
+ src_object_name, data = self._create_object(metadata)
+
+ # copy source object with x_fresh_metadata header
+ metadata = {'X-Fresh-Metadata': 'true'}
+ dst_object_name, resp = self._copy_object_2d(src_object_name,
+ metadata)
+
+ self.assertEqual(resp['status'], '201')
+ self.assertHeaders(resp, 'Object', 'COPY')
+
+ self.assertNotIn('x-object-meta-src', resp)
+ self.assertEqual(resp['x-copied-from'],
+ self.container_name + "/" + src_object_name)
+
+ # check that destination object does NOT have any object-meta
+ self._check_copied_obj(dst_object_name, data, not_in_meta=["src"])
+
+ @test.attr(type='smoke')
+ def test_copy_object_with_x_object_metakey(self):
+ # create source object
+ metadata = {'x-object-meta-src': 'src_value'}
+ src_obj_name, data = self._create_object(metadata)
+
+ # copy source object to destination with x-object-meta-key
+ metadata = {'x-object-meta-test': ''}
+ dst_obj_name, resp = self._copy_object_2d(src_obj_name, metadata)
+
+ self.assertEqual(resp['status'], '201')
+ self.assertHeaders(resp, 'Object', 'COPY')
+
+ expected = {'x-object-meta-test': '',
+ 'x-object-meta-src': 'src_value',
+ 'x-copied-from': self.container_name + "/" + src_obj_name}
+ for key, value in six.iteritems(expected):
+ self.assertIn(key, resp)
+ self.assertEqual(value, resp[key])
+
+ # check destination object
+ self._check_copied_obj(dst_obj_name, data, in_meta=["test", "src"])
+
+ @test.attr(type='smoke')
+ def test_copy_object_with_x_object_meta(self):
+ # create source object
+ metadata = {'x-object-meta-src': 'src_value'}
+ src_obj_name, data = self._create_object(metadata)
+
+ # copy source object to destination with object metadata
+ metadata = {'x-object-meta-test': 'value'}
+ dst_obj_name, resp = self._copy_object_2d(src_obj_name, metadata)
+
+ self.assertEqual(resp['status'], '201')
+ self.assertHeaders(resp, 'Object', 'COPY')
+
+ expected = {'x-object-meta-test': 'value',
+ 'x-object-meta-src': 'src_value',
+ 'x-copied-from': self.container_name + "/" + src_obj_name}
+ for key, value in six.iteritems(expected):
+ self.assertIn(key, resp)
+ self.assertEqual(value, resp[key])
+
+ # check destination object
+ self._check_copied_obj(dst_obj_name, data, in_meta=["test", "src"])
+
@test.attr(type='gate')
def test_object_upload_in_segments(self):
# create object
object_name = data_utils.rand_name(name='LObject')
data = data_utils.arbitrary_string()
segments = 10
- data_segments = [data + str(i) for i in moves.xrange(segments)]
+ data_segments = [data + str(i) for i in six.moves.xrange(segments)]
# uploading segments
- for i in moves.xrange(segments):
+ for i in six.moves.xrange(segments):
resp, _ = self.object_client.create_object_segments(
self.container_name, object_name, i, data_segments[i])
self.assertEqual(resp['status'], '201')
diff --git a/tempest/api/orchestration/stacks/test_nova_keypair_resources.py b/tempest/api/orchestration/stacks/test_nova_keypair_resources.py
index e22a08b..e3ffdaf 100644
--- a/tempest/api/orchestration/stacks/test_nova_keypair_resources.py
+++ b/tempest/api/orchestration/stacks/test_nova_keypair_resources.py
@@ -48,7 +48,7 @@
for resource in resources:
cls.test_resources[resource['logical_resource_id']] = resource
- @test.attr(type='slow')
+ @test.attr(type='gate')
def test_created_resources(self):
"""Verifies created keypair resource."""
@@ -68,7 +68,7 @@
self.assertEqual(resource_type, resource['resource_type'])
self.assertEqual('CREATE_COMPLETE', resource['resource_status'])
- @test.attr(type='slow')
+ @test.attr(type='gate')
def test_stack_keypairs_output(self):
resp, stack = self.client.get_stack(self.stack_name)
self.assertEqual('200', resp['status'])
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index abf3c6b..b7de767 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -71,6 +71,9 @@
msg = "Volume API v2 is disabled"
raise cls.skipException(msg)
cls.volumes_client = cls.os.volumes_v2_client
+ cls.volumes_extension_client = cls.os.volumes_v2_extension_client
+ cls.availability_zone_client = (
+ cls.os.volume_v2_availability_zone_client)
# Special fields and resp code for cinder v2
cls.special_fields = {'name_field': 'name',
'descrip_field': 'description',
diff --git a/tempest/api/volume/test_availability_zone.py b/tempest/api/volume/test_availability_zone.py
index fe8f96e..25b7b85 100644
--- a/tempest/api/volume/test_availability_zone.py
+++ b/tempest/api/volume/test_availability_zone.py
@@ -17,16 +17,15 @@
from tempest import test
-class AvailabilityZoneTestJSON(base.BaseVolumeV1Test):
+class AvailabilityZoneV2TestJSON(base.BaseVolumeTest):
"""
- Tests Availability Zone API List
+ Tests Availability Zone V2 API List
"""
- _interface = 'json'
@classmethod
def setUpClass(cls):
- super(AvailabilityZoneTestJSON, cls).setUpClass()
+ super(AvailabilityZoneV2TestJSON, cls).setUpClass()
cls.client = cls.availability_zone_client
@test.attr(type='gate')
@@ -37,5 +36,13 @@
self.assertTrue(len(availability_zone) > 0)
-class AvailabilityZoneTestXML(AvailabilityZoneTestJSON):
+class AvailabilityZoneV2TestXML(AvailabilityZoneV2TestJSON):
+ _interface = 'xml'
+
+
+class AvailabilityZoneV1TestJSON(AvailabilityZoneV2TestJSON):
+ _api_version = 1
+
+
+class AvailabilityZoneV1TestXML(AvailabilityZoneV1TestJSON):
_interface = 'xml'
diff --git a/tempest/api/volume/test_extensions.py b/tempest/api/volume/test_extensions.py
index ce019a2..ff00dd1 100644
--- a/tempest/api/volume/test_extensions.py
+++ b/tempest/api/volume/test_extensions.py
@@ -25,8 +25,7 @@
LOG = logging.getLogger(__name__)
-class ExtensionsTestJSON(base.BaseVolumeV1Test):
- _interface = 'json'
+class ExtensionsV2TestJSON(base.BaseVolumeTest):
@test.attr(type='gate')
def test_list_extensions(self):
@@ -46,5 +45,13 @@
raise self.skipException('There are not any extensions configured')
-class ExtensionsTestXML(ExtensionsTestJSON):
+class ExtensionsV2TestXML(ExtensionsV2TestJSON):
+ _interface = 'xml'
+
+
+class ExtensionsV1TestJSON(ExtensionsV2TestJSON):
+ _api_version = 1
+
+
+class ExtensionsV1TestXML(ExtensionsV1TestJSON):
_interface = 'xml'
diff --git a/tempest/api_schema/compute/v2/servers.py b/tempest/api_schema/compute/v2/servers.py
index 405ebe7..4abadf4 100644
--- a/tempest/api_schema/compute/v2/servers.py
+++ b/tempest/api_schema/compute/v2/servers.py
@@ -28,14 +28,11 @@
'id': {'type': 'string'},
'security_groups': {'type': 'array'},
'links': parameter_types.links,
- 'adminPass': {'type': 'string'},
'OS-DCF:diskConfig': {'type': 'string'}
},
# NOTE: OS-DCF:diskConfig is API extension, and some
# environments return a response without the attribute.
# So it is not 'required'.
- # NOTE: adminPass is not required because it can be deactivated
- # with nova API flag enable_instance_password=False
'required': ['id', 'security_groups', 'links']
}
},
@@ -43,6 +40,12 @@
}
}
+create_server_with_admin_pass = copy.deepcopy(create_server)
+create_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'adminPass': {'type': 'string'}})
+create_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('adminPass')
+
update_server = copy.deepcopy(servers.base_update_get_server)
update_server['response_body']['properties']['server']['properties'].update({
'hostId': {'type': 'string'},
diff --git a/tempest/api_schema/compute/v3/servers.py b/tempest/api_schema/compute/v3/servers.py
index 5f348e0..a84ac3c 100644
--- a/tempest/api_schema/compute/v3/servers.py
+++ b/tempest/api_schema/compute/v3/servers.py
@@ -28,7 +28,6 @@
'id': {'type': 'string'},
'os-security-groups:security_groups': {'type': 'array'},
'links': parameter_types.links,
- 'admin_password': {'type': 'string'},
'os-access-ips:access_ip_v4': parameter_types.access_ip_v4,
'os-access-ips:access_ip_v6': parameter_types.access_ip_v6
},
@@ -36,13 +35,19 @@
# and some environments return a response without these
# attributes. So they are not 'required'.
'required': ['id', 'os-security-groups:security_groups',
- 'links', 'admin_password']
+ 'links']
}
},
'required': ['server']
}
}
+create_server_with_admin_pass = copy.deepcopy(create_server)
+create_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'admin_password': {'type': 'string'}})
+create_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('admin_password')
+
addresses_v3 = copy.deepcopy(parameter_types.addresses)
addresses_v3['patternProperties']['^[a-zA-Z0-9-_.]+$']['items'][
'properties'].update({
diff --git a/tempest/clients.py b/tempest/clients.py
index 519e686..0edcdf4 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -181,7 +181,15 @@
ExtensionsClientJSON as VolumeExtensionClientJSON
from tempest.services.volume.json.snapshots_client import SnapshotsClientJSON
from tempest.services.volume.json.volumes_client import VolumesClientJSON
+from tempest.services.volume.v2.json.availability_zone_client import \
+ VolumeV2AvailabilityZoneClientJSON
+from tempest.services.volume.v2.json.extensions_client import \
+ ExtensionsV2ClientJSON as VolumeV2ExtensionClientJSON
from tempest.services.volume.v2.json.volumes_client import VolumesV2ClientJSON
+from tempest.services.volume.v2.xml.availability_zone_client import \
+ VolumeV2AvailabilityZoneClientXML
+from tempest.services.volume.v2.xml.extensions_client import \
+ ExtensionsV2ClientXML as VolumeV2ExtensionClientXML
from tempest.services.volume.v2.xml.volumes_client import VolumesV2ClientXML
from tempest.services.volume.xml.admin.volume_hosts_client import \
VolumeHostsClientXML
@@ -270,6 +278,8 @@
self.auth_provider)
self.volumes_extension_client = VolumeExtensionClientXML(
self.auth_provider)
+ self.volumes_v2_extension_client = VolumeV2ExtensionClientXML(
+ self.auth_provider)
if CONF.service_available.ceilometer:
self.telemetry_client = TelemetryClientXML(
self.auth_provider)
@@ -277,6 +287,8 @@
self.token_v3_client = V3TokenClientXML()
self.volume_availability_zone_client = \
VolumeAvailabilityZoneClientXML(self.auth_provider)
+ self.volume_v2_availability_zone_client = \
+ VolumeV2AvailabilityZoneClientXML(self.auth_provider)
elif self.interface == 'json':
self.certificates_client = CertificatesClientJSON(
@@ -362,6 +374,8 @@
self.auth_provider)
self.volumes_extension_client = VolumeExtensionClientJSON(
self.auth_provider)
+ self.volumes_v2_extension_client = VolumeV2ExtensionClientJSON(
+ self.auth_provider)
self.hosts_v3_client = HostsV3ClientJSON(self.auth_provider)
self.database_flavors_client = DatabaseFlavorsClientJSON(
self.auth_provider)
@@ -378,6 +392,8 @@
self.negative_client.service = service
self.volume_availability_zone_client = \
VolumeAvailabilityZoneClientJSON(self.auth_provider)
+ self.volume_v2_availability_zone_client = \
+ VolumeV2AvailabilityZoneClientJSON(self.auth_provider)
else:
msg = "Unsupported interface type `%s'" % interface
diff --git a/tempest/cmd/javelin.py b/tempest/cmd/javelin.py
index 67b92b0..3616a82 100755
--- a/tempest/cmd/javelin.py
+++ b/tempest/cmd/javelin.py
@@ -309,6 +309,14 @@
return name, fname
+def _get_image_by_name(client, name):
+ r, body = client.images.image_list()
+ for image in body:
+ if name == image['name']:
+ return image
+ return None
+
+
def create_images(images):
if not images:
return
@@ -317,9 +325,7 @@
client = client_for_user(image['owner'])
# only upload a new image if the name isn't there
- r, body = client.images.image_list()
- names = [x['name'] for x in body]
- if image['name'] in names:
+ if _get_image_by_name(client, image['name']):
LOG.info("Image '%s' already exists" % image['name'])
continue
@@ -345,6 +351,20 @@
client.images.store_image(image_id, open(fname, 'r'))
+def destroy_images(images):
+ if not images:
+ return
+ LOG.info("Destroying images")
+ for image in images:
+ client = client_for_user(image['owner'])
+
+ response = _get_image_by_name(client, image['name'])
+ if not response:
+ LOG.info("Image '%s' does not exists" % image['name'])
+ continue
+ client.images.delete_image(response['id'])
+
+
#######################
#
# SERVERS
@@ -359,14 +379,6 @@
return None
-def _get_image_by_name(client, name):
- r, body = client.images.image_list()
- for image in body:
- if name == image['name']:
- return image
- return None
-
-
def _get_flavor_by_name(client, name):
r, body = client.flavors.list_flavors()
for flavor in body:
@@ -478,13 +490,14 @@
# destroy_volumes
destroy_servers(RES['servers'])
- LOG.warn("Destroy mode incomplete")
- # destroy_images
+ destroy_images(RES['images'])
# destroy_objects
# destroy_users
# destroy_tenants
+ LOG.warn("Destroy mode incomplete")
+
def get_options():
global OPTS
diff --git a/tempest/common/custom_matchers.py b/tempest/common/custom_matchers.py
index 4a7921f..996c365 100644
--- a/tempest/common/custom_matchers.py
+++ b/tempest/common/custom_matchers.py
@@ -69,10 +69,24 @@
elif self.target == 'Object':
if 'etag' not in actual:
return NonExistentHeader('etag')
- elif self.method == 'PUT' or self.method == 'COPY':
+ if 'last-modified' not in actual:
+ return NonExistentHeader('last-modified')
+ elif self.method == 'PUT':
if self.target == 'Object':
if 'etag' not in actual:
return NonExistentHeader('etag')
+ if 'last-modified' not in actual:
+ return NonExistentHeader('last-modified')
+ elif self.method == 'COPY':
+ if self.target == 'Object':
+ if 'etag' not in actual:
+ return NonExistentHeader('etag')
+ if 'last-modified' not in actual:
+ return NonExistentHeader('last-modified')
+ if 'x-copied-from' not in actual:
+ return NonExistentHeader('x-copied-from')
+ if 'x-copied-from-last-modified' not in actual:
+ return NonExistentHeader('x-copied-from-last-modified')
return None
@@ -122,11 +136,17 @@
return InvalidFormat(key, value)
elif key == 'content-type' and not value:
return InvalidFormat(key, value)
+ elif key == 'x-copied-from' and not re.match("\S+/\S+", value):
+ return InvalidFormat(key, value)
+ elif key == 'x-copied-from-last-modified' and not value:
+ return InvalidFormat(key, value)
elif key == 'x-trans-id' and \
not re.match("^tx[0-9a-f]{21}-[0-9a-f]{10}.*", value):
return InvalidFormat(key, value)
elif key == 'date' and not value:
return InvalidFormat(key, value)
+ elif key == 'last-modified' and not value:
+ return InvalidFormat(key, value)
elif key == 'accept-ranges' and not value == 'bytes':
return InvalidFormat(key, value)
elif key == 'etag' and not value.isalnum():
diff --git a/tempest/config.py b/tempest/config.py
index 01bc243..db54269 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -286,6 +286,9 @@
cfg.BoolOpt('pause',
default=True,
help="Does the test environment support pausing?"),
+ cfg.BoolOpt('shelve',
+ default=True,
+ help="Does the test environment support shelving/unshelving?"),
cfg.BoolOpt('suspend',
default=True,
help="Does the test environment support suspend/resume?"),
diff --git a/tempest/services/compute/json/servers_client.py b/tempest/services/compute/json/servers_client.py
index a0ffc91..a4e3641 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -93,7 +93,11 @@
# with return reservation id set True
if 'reservation_id' in body:
return resp, body
- self.validate_response(schema.create_server, resp, body)
+ if CONF.compute_feature_enabled.enable_instance_password:
+ create_schema = schema.create_server_with_admin_pass
+ else:
+ create_schema = schema.create_server
+ self.validate_response(create_schema, resp, body)
return resp, body['server']
def update_server(self, server_id, name=None, meta=None, accessIPv4=None,
diff --git a/tempest/services/compute/v3/json/servers_client.py b/tempest/services/compute/v3/json/servers_client.py
index 51c4499..c3fd355 100644
--- a/tempest/services/compute/v3/json/servers_client.py
+++ b/tempest/services/compute/v3/json/servers_client.py
@@ -96,7 +96,11 @@
# with return reservation id set True
if 'servers_reservation' in body:
return resp, body['servers_reservation']
- self.validate_response(schema.create_server, resp, body)
+ if CONF.compute_feature_enabled.enable_instance_password:
+ create_schema = schema.create_server_with_admin_pass
+ else:
+ create_schema = schema.create_server
+ self.validate_response(create_schema, resp, body)
return resp, body['server']
def update_server(self, server_id, name=None, meta=None, access_ip_v4=None,
diff --git a/tempest/services/data_processing/v1_1/client.py b/tempest/services/data_processing/v1_1/client.py
index 1fe0cf1..7acbae7 100644
--- a/tempest/services/data_processing/v1_1/client.py
+++ b/tempest/services/data_processing/v1_1/client.py
@@ -258,3 +258,38 @@
uri = 'job-binaries/%s/data' % job_binary_id
return self._request_and_check_resp(self.get, uri, 200)
+
+ def list_jobs(self):
+ """List all jobs for a user."""
+
+ uri = 'jobs'
+ return self._request_check_and_parse_resp(self.get, uri, 200, 'jobs')
+
+ def get_job(self, job_id):
+ """Returns the details of a single job."""
+
+ uri = 'jobs/%s' % job_id
+ return self._request_check_and_parse_resp(self.get, uri, 200, 'job')
+
+ def create_job(self, name, job_type, mains, libs=None, **kwargs):
+ """Creates job with specified params.
+
+ It supports passing additional params using kwargs and returns created
+ object.
+ """
+ uri = 'jobs'
+ body = kwargs.copy()
+ body.update({
+ 'name': name,
+ 'type': job_type,
+ 'mains': mains,
+ 'libs': libs or list(),
+ })
+ return self._request_check_and_parse_resp(self.post, uri, 202,
+ 'job', body=json.dumps(body))
+
+ def delete_job(self, job_id):
+ """Deletes the specified job by id."""
+
+ uri = 'jobs/%s' % job_id
+ return self._request_and_check_resp(self.delete, uri, 204)
diff --git a/tempest/services/identity/v3/json/identity_client.py b/tempest/services/identity/v3/json/identity_client.py
index 0188c2a..d57b931 100644
--- a/tempest/services/identity/v3/json/identity_client.py
+++ b/tempest/services/identity/v3/json/identity_client.py
@@ -135,8 +135,11 @@
body = json.loads(body)
return resp, body['project']
- def list_projects(self):
- resp, body = self.get("projects")
+ def list_projects(self, params=None):
+ url = "projects"
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
self.expected_success(200, resp.status)
body = json.loads(body)
return resp, body['projects']
diff --git a/tempest/services/identity/v3/xml/identity_client.py b/tempest/services/identity/v3/xml/identity_client.py
index f3e084e..c2bd77e 100644
--- a/tempest/services/identity/v3/xml/identity_client.py
+++ b/tempest/services/identity/v3/xml/identity_client.py
@@ -197,9 +197,12 @@
body = self._parse_body(etree.fromstring(body))
return resp, body
- def list_projects(self):
+ def list_projects(self, params=None):
"""Get the list of projects."""
- resp, body = self.get("projects")
+ url = 'projects'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
self.expected_success(200, resp.status)
body = self._parse_projects(etree.fromstring(body))
return resp, body
diff --git a/tempest/services/volume/json/availability_zone_client.py b/tempest/services/volume/json/availability_zone_client.py
index 6839d3a..f2e7c5c 100644
--- a/tempest/services/volume/json/availability_zone_client.py
+++ b/tempest/services/volume/json/availability_zone_client.py
@@ -21,10 +21,10 @@
CONF = config.CONF
-class VolumeAvailabilityZoneClientJSON(rest_client.RestClient):
+class BaseVolumeAvailabilityZoneClientJSON(rest_client.RestClient):
def __init__(self, auth_provider):
- super(VolumeAvailabilityZoneClientJSON, self).__init__(
+ super(BaseVolumeAvailabilityZoneClientJSON, self).__init__(
auth_provider)
self.service = CONF.volume.catalog_type
@@ -32,3 +32,9 @@
resp, body = self.get('os-availability-zone')
body = json.loads(body)
return resp, body['availabilityZoneInfo']
+
+
+class VolumeAvailabilityZoneClientJSON(BaseVolumeAvailabilityZoneClientJSON):
+ """
+ Volume V1 availability zone client.
+ """
diff --git a/tempest/services/volume/json/extensions_client.py b/tempest/services/volume/json/extensions_client.py
index 9e182ea..e3ff00b 100644
--- a/tempest/services/volume/json/extensions_client.py
+++ b/tempest/services/volume/json/extensions_client.py
@@ -21,10 +21,10 @@
CONF = config.CONF
-class ExtensionsClientJSON(rest_client.RestClient):
+class BaseExtensionsClientJSON(rest_client.RestClient):
def __init__(self, auth_provider):
- super(ExtensionsClientJSON, self).__init__(auth_provider)
+ super(BaseExtensionsClientJSON, self).__init__(auth_provider)
self.service = CONF.volume.catalog_type
def list_extensions(self):
@@ -32,3 +32,9 @@
resp, body = self.get(url)
body = json.loads(body)
return resp, body['extensions']
+
+
+class ExtensionsClientJSON(BaseExtensionsClientJSON):
+ """
+ Volume V1 extensions client.
+ """
diff --git a/tempest/services/volume/v2/json/availability_zone_client.py b/tempest/services/volume/v2/json/availability_zone_client.py
new file mode 100644
index 0000000..047ba1b
--- /dev/null
+++ b/tempest/services/volume/v2/json/availability_zone_client.py
@@ -0,0 +1,26 @@
+# Copyright 2014 IBM Corp.
+# 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.services.volume.json import availability_zone_client
+
+
+class VolumeV2AvailabilityZoneClientJSON(
+ availability_zone_client.BaseVolumeAvailabilityZoneClientJSON):
+
+ def __init__(self, auth_provider):
+ super(VolumeV2AvailabilityZoneClientJSON, self).__init__(
+ auth_provider)
+
+ self.api_version = "v2"
diff --git a/tempest/services/volume/v2/json/extensions_client.py b/tempest/services/volume/v2/json/extensions_client.py
new file mode 100644
index 0000000..cc5244c
--- /dev/null
+++ b/tempest/services/volume/v2/json/extensions_client.py
@@ -0,0 +1,24 @@
+# Copyright 2014 IBM Corp.
+# 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.services.volume.json import extensions_client
+
+
+class ExtensionsV2ClientJSON(extensions_client.BaseExtensionsClientJSON):
+
+ def __init__(self, auth_provider):
+ super(ExtensionsV2ClientJSON, self).__init__(auth_provider)
+
+ self.api_version = "v2"
diff --git a/tempest/services/volume/v2/xml/availability_zone_client.py b/tempest/services/volume/v2/xml/availability_zone_client.py
new file mode 100644
index 0000000..68ca39b
--- /dev/null
+++ b/tempest/services/volume/v2/xml/availability_zone_client.py
@@ -0,0 +1,26 @@
+# Copyright 2014 IBM Corp.
+# 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.services.volume.xml import availability_zone_client
+
+
+class VolumeV2AvailabilityZoneClientXML(
+ availability_zone_client.BaseVolumeAvailabilityZoneClientXML):
+
+ def __init__(self, auth_provider):
+ super(VolumeV2AvailabilityZoneClientXML, self).__init__(
+ auth_provider)
+
+ self.api_version = "v2"
diff --git a/tempest/services/volume/v2/xml/extensions_client.py b/tempest/services/volume/v2/xml/extensions_client.py
new file mode 100644
index 0000000..13f333c
--- /dev/null
+++ b/tempest/services/volume/v2/xml/extensions_client.py
@@ -0,0 +1,24 @@
+# Copyright 2014 IBM Corp.
+# 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.services.volume.xml import extensions_client
+
+
+class ExtensionsV2ClientXML(extensions_client.BaseExtensionsClientXML):
+
+ def __init__(self, auth_provider):
+ super(ExtensionsV2ClientXML, self).__init__(auth_provider)
+
+ self.api_version = "v2"
diff --git a/tempest/services/volume/xml/availability_zone_client.py b/tempest/services/volume/xml/availability_zone_client.py
index e4a004a..a883ef5 100644
--- a/tempest/services/volume/xml/availability_zone_client.py
+++ b/tempest/services/volume/xml/availability_zone_client.py
@@ -22,11 +22,11 @@
CONF = config.CONF
-class VolumeAvailabilityZoneClientXML(rest_client.RestClient):
+class BaseVolumeAvailabilityZoneClientXML(rest_client.RestClient):
TYPE = "xml"
def __init__(self, auth_provider):
- super(VolumeAvailabilityZoneClientXML, self).__init__(
+ super(BaseVolumeAvailabilityZoneClientXML, self).__init__(
auth_provider)
self.service = CONF.volume.catalog_type
@@ -37,3 +37,9 @@
resp, body = self.get('os-availability-zone')
availability_zone = self._parse_array(etree.fromstring(body))
return resp, availability_zone
+
+
+class VolumeAvailabilityZoneClientXML(BaseVolumeAvailabilityZoneClientXML):
+ """
+ Volume V1 availability zone client.
+ """
diff --git a/tempest/services/volume/xml/extensions_client.py b/tempest/services/volume/xml/extensions_client.py
index 2986fcd..fe8b7cb 100644
--- a/tempest/services/volume/xml/extensions_client.py
+++ b/tempest/services/volume/xml/extensions_client.py
@@ -22,11 +22,11 @@
CONF = config.CONF
-class ExtensionsClientXML(rest_client.RestClient):
+class BaseExtensionsClientXML(rest_client.RestClient):
TYPE = "xml"
def __init__(self, auth_provider):
- super(ExtensionsClientXML, self).__init__(auth_provider)
+ super(BaseExtensionsClientXML, self).__init__(auth_provider)
self.service = CONF.volume.catalog_type
def _parse_array(self, node):
@@ -40,3 +40,9 @@
resp, body = self.get(url)
body = self._parse_array(etree.fromstring(body))
return resp, body
+
+
+class ExtensionsClientXML(BaseExtensionsClientXML):
+ """
+ Volume V1 extensions client.
+ """
diff --git a/tox.ini b/tox.ini
index 4f2465a..9c32121 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,23 +6,28 @@
[testenv]
setenv = VIRTUAL_ENV={envdir}
OS_TEST_PATH=./tempest/test_discover
+ PYTHONHASHSEED=0
usedevelop = True
install_command = pip install -U {opts} {packages}
[testenv:py26]
setenv = OS_TEST_PATH=./tempest/tests
+ PYTHONHASHSEED=0
commands = python setup.py test --slowest --testr-arg='tempest\.tests {posargs}'
[testenv:py33]
setenv = OS_TEST_PATH=./tempest/tests
+ PYTHONHASHSEED=0
commands = python setup.py test --slowest --testr-arg='tempest\.tests {posargs}'
[testenv:py27]
setenv = OS_TEST_PATH=./tempest/tests
+ PYTHONHASHSEED=0
commands = python setup.py test --slowest --testr-arg='tempest\.tests {posargs}'
[testenv:cover]
setenv = OS_TEST_PATH=./tempest/tests
+ PYTHONHASHSEED=0
commands = python setup.py testr --coverage --testr-arg='tempest\.tests {posargs}'
[testenv:all]