Merge "Add 2.55 schema & update flavor API in flavors_client"
diff --git a/.zuul.yaml b/.zuul.yaml
index e4e95f2..8dcb935 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -81,6 +81,26 @@
# without Swift, c-bak cannot run (in the Gate at least)
c-bak: false
+- nodeset:
+ name: openstack-bionic-node
+ nodes:
+ - name: controller
+ label: ubuntu-bionic
+ groups:
+ - name: tempest
+ nodes:
+ - controller
+
+- job:
+ name: tempest-full-py36
+ parent: tempest-full-py3
+ nodeset: openstack-bionic-node
+ branches:
+ - master
+ description: |
+ Base integration test with Neutron networking and py36.
+ voting: false
+
- job:
name: tempest-full-queens
parent: tempest-full
@@ -198,6 +218,16 @@
- ^setup.cfg$
- ^tempest/hacking/.*$
- ^tempest/tests/.*$
+ - tempest-full-py36:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
- tempest-full-queens:
irrelevant-files:
- ^(test-|)requirements.txt$
@@ -244,6 +274,16 @@
- ^setup.cfg$
- ^tempest/hacking/.*$
- ^tempest/tests/.*$
+ - nova-live-migration:
+ irrelevant-files:
+ - ^(test-|)requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^etc/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tempest/hacking/.*$
+ - ^tempest/tests/.*$
periodic-stable:
jobs:
- tempest-full-queens
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 535dfc6..325b19f 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -306,10 +306,18 @@
.. _2.6: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id5
+ * `2.9`_
+
+ .. _2.9: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id8
+
* `2.10`_
.. _2.10: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id9
+ * `2.19`_
+
+ .. _2.19: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id17
+
* `2.20`_
.. _2.20: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id18
@@ -322,6 +330,10 @@
.. _2.25: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-mitaka
+ * `2.26`_
+
+ .. _2.26: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id23
+
* `2.32`_
.. _2.32: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id29
diff --git a/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml b/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml
new file mode 100644
index 0000000..53125ef
--- /dev/null
+++ b/releasenotes/notes/bp-application-credentials-df69b1f617db1bb9.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - |
+ [`blueprint application-credentials <https://blueprints.launchpad.net/keystone/+spec/application-credentials>`_]
+ Tempest can test keystone's application credentials interface. A new client
+ library is added for application credentials, and a new config option,
+ ``[identity-feature-enabled]/application_credentials``, can control whether
+ the application credentials feature is tested (defaults to False,
+ indicating the feature is not enabled in the cloud under test).
diff --git a/setup.cfg b/setup.cfg
index c981370..96ee7ea 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -47,12 +47,5 @@
oslo.config.opts =
tempest.config = tempest.config:list_opts
-[build_sphinx]
-all-files = 1
-# warning can be generated by using GENERATE_TEMPEST_PLUGIN_LIST='False'
-warning-is-error = 0
-build-dir = doc/build
-source-dir = doc/source
-
[wheel]
universal = 1
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 2398cf1..bc38144 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -147,6 +147,7 @@
@testtools.skipIf(not CONF.compute_feature_enabled.
block_migrate_cinder_iscsi,
'Block Live migration not configured for iSCSI')
+ @utils.services('volume')
def test_iscsi_volume(self):
server = self.create_test_server(wait_until="ACTIVE")
server_id = server['id']
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index 3f06c4e..cdfc44a 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -16,6 +16,7 @@
from tempest.common import waiters
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
class ServersAdminTestJSON(base.BaseV2ComputeAdminTest):
@@ -61,9 +62,13 @@
@decorators.idempotent_id('d56e9540-73ed-45e0-9b88-98fc419087eb')
def test_list_servers_detailed_filter_by_invalid_status(self):
params = {'status': 'invalid_status'}
- body = self.client.list_servers(detail=True, **params)
- servers = body['servers']
- self.assertEmpty(servers)
+ if self.is_requested_microversion_compatible('2.37'):
+ body = self.client.list_servers(detail=True, **params)
+ servers = body['servers']
+ self.assertEmpty(servers)
+ else:
+ self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
+ detail=True, **params)
@decorators.idempotent_id('51717b38-bdc1-458b-b636-1cf82d99f62f')
def test_list_servers_by_admin(self):
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 760e356..975728c 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -127,6 +127,36 @@
cls.image_ssh_password = CONF.validation.image_ssh_password
@classmethod
+ def is_requested_microversion_compatible(cls, max_version):
+ """Check the compatibility of selected request microversion
+
+ This method will check if selected request microversion
+ (cls.request_microversion) for test is compatible with respect
+ to 'max_version'. Compatible means if selected request microversion
+ is in the range(<=) of 'max_version'.
+
+ :param max_version: maximum microversion to compare for compatibility.
+ Example: '2.30'
+ :returns: True if selected request microversion is compatible with
+ 'max_version'. False in other case.
+ """
+ try:
+ req_version_obj = api_version_request.APIVersionRequest(
+ cls.request_microversion)
+ # NOTE(gmann): This is case where this method is used before calling
+ # resource_setup(), where cls.request_microversion is set. There may
+ # not be any such case but still we can handle this case.
+ except AttributeError:
+ request_microversion = (
+ api_version_utils.select_request_microversion(
+ cls.min_microversion,
+ CONF.compute.min_microversion))
+ req_version_obj = api_version_request.APIVersionRequest(
+ request_microversion)
+ max_version_obj = api_version_request.APIVersionRequest(max_version)
+ return req_version_obj <= max_version_obj
+
+ @classmethod
def server_check_teardown(cls):
"""Checks is the shared server clean enough for subsequent test.
diff --git a/tempest/api/compute/servers/test_list_servers_negative.py b/tempest/api/compute/servers/test_list_servers_negative.py
index 393e68f..18a78f0 100644
--- a/tempest/api/compute/servers/test_list_servers_negative.py
+++ b/tempest/api/compute/servers/test_list_servers_negative.py
@@ -79,10 +79,16 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('fcdf192d-0f74-4d89-911f-1ec002b822c4')
def test_list_servers_status_non_existing(self):
- # Return an empty list when invalid status is specified
- body = self.client.list_servers(status='non_existing_status')
- servers = body['servers']
- self.assertEmpty(servers)
+ # When invalid status is specified, up to microversion 2.37,
+ # an empty list is returnd, and starting from microversion 2.38,
+ # a 400 error is returned in that case.
+ if self.is_requested_microversion_compatible('2.37'):
+ body = self.client.list_servers(status='non_existing_status')
+ servers = body['servers']
+ self.assertEmpty(servers)
+ else:
+ self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
+ status='non_existing_status')
@decorators.attr(type=['negative'])
@decorators.idempotent_id('d47c17fb-eebd-4287-8e95-f20a7e627b18')
diff --git a/tempest/api/compute/servers/test_servers.py b/tempest/api/compute/servers/test_servers.py
index 2904976..543fa1c 100644
--- a/tempest/api/compute/servers/test_servers.py
+++ b/tempest/api/compute/servers/test_servers.py
@@ -184,8 +184,28 @@
min_microversion = '2.47'
max_microversion = 'latest'
+ # NOTE(gmann): This test tests the server APIs response schema
+ # Along with 2.47 microversion schema this test class tests the
+ # other microversions 2.9, 2.19 and 2.26 server APIs response schema
+ # also. 2.47 APIs schema are on top of 2.9->2.19->2.26 schema so
+ # below tests cover all of the schema.
+
@decorators.idempotent_id('88b0bdb2-494c-11e7-a919-92ebcb67fe33')
def test_show_server(self):
server = self.create_test_server()
# All fields will be checked by API schema
self.servers_client.show_server(server['id'])
+
+ @decorators.idempotent_id('8de397c2-57d0-4b90-aa30-e5d668f21a8b')
+ def test_update_rebuild_list_server(self):
+ server = self.create_test_server()
+ # Checking update API response schema
+ self.servers_client.update_server(server['id'])
+ waiters.wait_for_server_status(self.servers_client, server['id'],
+ 'ACTIVE')
+ # Checking rebuild API response schema
+ self.servers_client.rebuild_server(server['id'], self.image_ref_alt)
+ waiters.wait_for_server_status(self.servers_client,
+ server['id'], 'ACTIVE')
+ # Checking list details API response schema
+ self.servers_client.list_servers(detail=True)
diff --git a/tempest/api/identity/admin/v2/test_tenants.py b/tempest/api/identity/admin/v2/test_tenants.py
index 0f955bf..cda721c 100644
--- a/tempest/api/identity/admin/v2/test_tenants.py
+++ b/tempest/api/identity/admin/v2/test_tenants.py
@@ -59,11 +59,9 @@
# Create a tenant that is enabled
tenant = self.setup_test_tenant(enabled=True)
tenant_id = tenant['id']
- en1 = tenant['enabled']
- self.assertTrue(en1, 'Enable should be True in response')
+ self.assertTrue(tenant['enabled'], 'Enable should be True in response')
body = self.tenants_client.show_tenant(tenant_id)['tenant']
- en2 = body['enabled']
- self.assertTrue(en2, 'Enable should be True in lookup')
+ self.assertTrue(body['enabled'], 'Enable should be True in lookup')
self.tenants_client.delete_tenant(tenant_id)
@decorators.idempotent_id('3be22093-b30f-499d-b772-38340e5e16fb')
@@ -71,12 +69,10 @@
# Create a tenant that is not enabled
tenant = self.setup_test_tenant(enabled=False)
tenant_id = tenant['id']
- en1 = tenant['enabled']
- self.assertEqual('false', str(en1).lower(),
+ self.assertFalse(tenant['enabled'],
'Enable should be False in response')
body = self.tenants_client.show_tenant(tenant_id)['tenant']
- en2 = body['enabled']
- self.assertEqual('false', str(en2).lower(),
+ self.assertFalse(body['enabled'],
'Enable should be False in lookup')
self.tenants_client.delete_tenant(tenant_id)
@@ -143,7 +139,7 @@
resp3_en = body['enabled']
self.assertNotEqual(resp1_en, resp3_en)
- self.assertEqual('false', str(resp1_en).lower())
+ self.assertFalse(tenant['enabled'])
self.assertEqual(resp2_en, resp3_en)
self.tenants_client.delete_tenant(t_id)
diff --git a/tempest/api/identity/admin/v3/test_application_credentials.py b/tempest/api/identity/admin/v3/test_application_credentials.py
new file mode 100644
index 0000000..4a74ef8
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_application_credentials.py
@@ -0,0 +1,48 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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 import config
+from tempest.lib import decorators
+
+
+CONF = config.CONF
+
+
+class ApplicationCredentialsV3AdminTest(base.BaseApplicationCredentialsV3Test,
+ base.BaseIdentityV3AdminTest):
+
+ @decorators.idempotent_id('3b3dd48f-3388-406a-a9e6-4d078a552d0e')
+ def test_create_application_credential_with_roles(self):
+ role = self.setup_test_role()
+ self.os_admin.roles_v3_client.create_user_role_on_project(
+ self.project_id,
+ self.user_id,
+ role['id']
+ )
+
+ app_cred = self.create_application_credential(
+ roles=[{'id': role['id']}])
+ secret = app_cred['secret']
+
+ # Check that the application credential is functional
+ token_id, resp = self.non_admin_token.get_token(
+ app_cred_id=app_cred['id'],
+ app_cred_secret=secret,
+ auth_data=True
+ )
+ self.assertEqual(resp['project']['id'], self.project_id)
+ self.assertEqual(resp['roles'][0]['id'], role['id'])
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index ac23067..bc94a8e 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -101,22 +101,19 @@
# Create a project that is enabled
project = self.setup_test_project(enabled=True)
project_id = project['id']
- en1 = project['enabled']
- self.assertTrue(en1, 'Enable should be True in response')
+ self.assertTrue(project['enabled'],
+ 'Enable should be True in response')
body = self.projects_client.show_project(project_id)['project']
- en2 = body['enabled']
- self.assertTrue(en2, 'Enable should be True in lookup')
+ self.assertTrue(body['enabled'], 'Enable should be True in lookup')
@decorators.idempotent_id('78f96a9c-e0e0-4ee6-a3ba-fbf6dfd03207')
def test_project_create_not_enabled(self):
# Create a project that is not enabled
project = self.setup_test_project(enabled=False)
- en1 = project['enabled']
- self.assertEqual('false', str(en1).lower(),
+ self.assertFalse(project['enabled'],
'Enable should be False in response')
body = self.projects_client.show_project(project['id'])['project']
- en2 = body['enabled']
- self.assertEqual('false', str(en2).lower(),
+ self.assertFalse(body['enabled'],
'Enable should be False in lookup')
@decorators.idempotent_id('f608f368-048c-496b-ad63-d286c26dab6b')
@@ -178,7 +175,7 @@
resp3_en = body['enabled']
self.assertNotEqual(resp1_en, resp3_en)
- self.assertEqual('false', str(resp1_en).lower())
+ self.assertFalse(project['enabled'])
self.assertEqual(resp2_en, resp3_en)
@decorators.idempotent_id('59398d4a-5dc5-4f86-9a4c-c26cc804d6c6')
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 6edb8f3..68f2c07 100644
--- a/tempest/api/identity/base.py
+++ b/tempest/api/identity/base.py
@@ -190,6 +190,8 @@
cls.non_admin_catalog_client = cls.os_primary.catalog_client
cls.non_admin_versions_client =\
cls.os_primary.identity_versions_v3_client
+ cls.non_admin_app_creds_client = \
+ cls.os_primary.application_credentials_client
class BaseIdentityV3AdminTest(BaseIdentityV3Test):
@@ -289,3 +291,30 @@
test_utils.call_and_ignore_notfound_exc,
self.delete_domain, domain['id'])
return domain
+
+
+class BaseApplicationCredentialsV3Test(BaseIdentityV3Test):
+
+ @classmethod
+ def skip_checks(cls):
+ super(BaseApplicationCredentialsV3Test, cls).skip_checks()
+ if not CONF.identity_feature_enabled.application_credentials:
+ raise cls.skipException("Application credentials are not available"
+ " in this environment")
+
+ @classmethod
+ def resource_setup(cls):
+ super(BaseApplicationCredentialsV3Test, cls).resource_setup()
+ cls.user_id = cls.os_primary.credentials.user_id
+ cls.project_id = cls.os_primary.credentials.project_id
+
+ def create_application_credential(self, name=None, **kwargs):
+ name = name or data_utils.rand_name('application_credential')
+ application_credential = (
+ self.non_admin_app_creds_client.create_application_credential(
+ self.user_id, name=name, **kwargs))['application_credential']
+ self.addCleanup(
+ self.non_admin_app_creds_client.delete_application_credential,
+ self.user_id,
+ application_credential['id'])
+ return application_credential
diff --git a/tempest/api/identity/v3/test_application_credentials.py b/tempest/api/identity/v3/test_application_credentials.py
new file mode 100644
index 0000000..caf0b1e
--- /dev/null
+++ b/tempest/api/identity/v3/test_application_credentials.py
@@ -0,0 +1,85 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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 datetime
+
+from oslo_utils import timeutils
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib import decorators
+
+
+CONF = config.CONF
+
+
+class ApplicationCredentialsV3Test(base.BaseApplicationCredentialsV3Test):
+
+ def _list_app_creds(self, name=None):
+ kwargs = dict(user_id=self.user_id)
+ if name:
+ kwargs.update(name=name)
+ return self.non_admin_app_creds_client.list_application_credentials(
+ **kwargs)['application_credentials']
+
+ @decorators.idempotent_id('8080c75c-eddc-4786-941a-c2da7039ae61')
+ def test_create_application_credential(self):
+ app_cred = self.create_application_credential()
+
+ # Check that the secret appears in the create response
+ secret = app_cred['secret']
+
+ # Check that the secret is not retrievable after initial create
+ app_cred = self.non_admin_app_creds_client.show_application_credential(
+ user_id=self.user_id,
+ application_credential_id=app_cred['id']
+ )['application_credential']
+ self.assertNotIn('secret', app_cred)
+
+ # Check that the application credential is functional
+ token_id, resp = self.non_admin_token.get_token(
+ app_cred_id=app_cred['id'],
+ app_cred_secret=secret,
+ auth_data=True
+ )
+ self.assertEqual(resp['project']['id'], self.project_id)
+
+ @decorators.idempotent_id('852daf0c-42b5-4239-8466-d193d0543ed3')
+ def test_create_application_credential_expires(self):
+ expires_at = timeutils.utcnow() + datetime.timedelta(hours=1)
+
+ app_cred = self.create_application_credential(expires_at=expires_at)
+
+ expires_str = expires_at.isoformat()
+ self.assertEqual(expires_str, app_cred['expires_at'])
+
+ @decorators.idempotent_id('ff0cd457-6224-46e7-b79e-0ada4964a8a6')
+ def test_list_application_credentials(self):
+ self.create_application_credential()
+ self.create_application_credential()
+
+ app_creds = self._list_app_creds()
+ self.assertEqual(2, len(app_creds))
+
+ @decorators.idempotent_id('9bb5e5cc-5250-493a-8869-8b665f6aa5f6')
+ def test_query_application_credentials(self):
+ self.create_application_credential()
+ app_cred_two = self.create_application_credential()
+ app_cred_two_name = app_cred_two['name']
+
+ app_creds = self._list_app_creds(name=app_cred_two_name)
+ self.assertEqual(1, len(app_creds))
+ self.assertEqual(app_cred_two_name, app_creds[0]['name'])
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index ce5bd3e..aa57daf 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -71,8 +71,12 @@
self.assertEqual(1024, body.get('size'))
# Now try get image file
+ # NOTE: This Glance API returns different status codes for image
+ # condition. In this non-empty data case, Glance should return 200,
+ # so here should check the status code.
body = self.client.show_image_file(image['id'])
self.assertEqual(file_content, body.data)
+ self.assertEqual(200, body.response.status)
@decorators.attr(type='smoke')
@decorators.idempotent_id('f848bb94-1c6e-45a4-8726-39e3a5b23535')
@@ -111,6 +115,13 @@
visibility='private')
self.assertEqual('queued', image['status'])
+ # NOTE: This Glance API returns different status codes for image
+ # condition. In this empty data case, Glance should return 204,
+ # so here should check the status code.
+ image_file = self.client.show_image_file(image['id'])
+ self.assertEqual(0, len(image_file.data))
+ self.assertEqual(204, image_file.response.status)
+
# Now try uploading an image file
image_file = six.BytesIO(data_utils.random_bytes())
self.client.store_image_file(image['id'], image_file)
diff --git a/tempest/api/volume/admin/test_volume_retype_with_migration.py b/tempest/api/volume/admin/test_volume_retype_with_migration.py
index f0b3a4f..025c1be 100644
--- a/tempest/api/volume/admin/test_volume_retype_with_migration.py
+++ b/tempest/api/volume/admin/test_volume_retype_with_migration.py
@@ -46,13 +46,10 @@
extra_specs_src = {"volume_backend_name": backend_src}
extra_specs_dst = {"volume_backend_name": backend_dst}
- src_vol_type = cls.create_volume_type(extra_specs=extra_specs_src)
+ cls.src_vol_type = cls.create_volume_type(extra_specs=extra_specs_src)
cls.dst_vol_type = cls.create_volume_type(extra_specs=extra_specs_dst)
- cls.src_vol = cls.create_volume(volume_type=src_vol_type['name'])
-
- @classmethod
- def resource_cleanup(cls):
+ def _wait_for_internal_volume_cleanup(self, vol):
# When retyping a volume, Cinder creates an internal volume in the
# target backend. The volume in the source backend is deleted after
# the migration, so we need to wait for Cinder delete this volume
@@ -60,40 +57,37 @@
# This list should return 2 volumes until the copy and cleanup
# process is finished.
- fetched_list = cls.admin_volume_client.list_volumes(
+ fetched_list = self.admin_volume_client.list_volumes(
params={'all_tenants': True,
- 'display_name': cls.src_vol['name']})['volumes']
+ 'display_name': vol['name']})['volumes']
for fetched_vol in fetched_list:
- if fetched_vol['id'] != cls.src_vol['id']:
+ if fetched_vol['id'] != vol['id']:
# This is the Cinder internal volume
LOG.debug('Waiting for internal volume %s deletion',
fetched_vol['id'])
- cls.admin_volume_client.wait_for_resource_deletion(
+ self.admin_volume_client.wait_for_resource_deletion(
fetched_vol['id'])
break
- super(VolumeRetypeWithMigrationTest, cls).resource_cleanup()
-
- @decorators.idempotent_id('a1a41f3f-9dad-493e-9f09-3ff197d477cd')
- def test_available_volume_retype_with_migration(self):
-
+ def _retype_volume(self, volume):
keys_with_no_change = ('id', 'size', 'description', 'name', 'user_id',
'os-vol-tenant-attr:tenant_id')
keys_with_change = ('volume_type', 'os-vol-host-attr:host')
volume_source = self.admin_volume_client.show_volume(
- self.src_vol['id'])['volume']
+ volume['id'])['volume']
self.volumes_client.retype_volume(
- self.src_vol['id'],
+ volume['id'],
new_type=self.dst_vol_type['name'],
migration_policy='on-demand')
-
- waiters.wait_for_volume_retype(self.volumes_client, self.src_vol['id'],
+ self.addCleanup(self._wait_for_internal_volume_cleanup, volume)
+ waiters.wait_for_volume_retype(self.volumes_client, volume['id'],
self.dst_vol_type['name'])
+
volume_dest = self.admin_volume_client.show_volume(
- self.src_vol['id'])['volume']
+ volume['id'])['volume']
# Check the volume information after the migration.
self.assertEqual('success',
@@ -105,3 +99,27 @@
for key in keys_with_change:
self.assertNotEqual(volume_source[key], volume_dest[key])
+
+ @decorators.idempotent_id('a1a41f3f-9dad-493e-9f09-3ff197d477cd')
+ def test_available_volume_retype_with_migration(self):
+ src_vol = self.create_volume(volume_type=self.src_vol_type['name'])
+ self._retype_volume(src_vol)
+
+ @decorators.idempotent_id('d0d9554f-e7a5-4104-8973-f35b27ccb60d')
+ def test_volume_from_snapshot_retype_with_migration(self):
+ # Create a volume in the first backend
+ src_vol = self.create_volume(volume_type=self.src_vol_type['name'])
+
+ # Create a volume snapshot
+ snapshot = self.create_snapshot(src_vol['id'])
+
+ # Create a volume from the snapshot
+ src_vol = self.create_volume(volume_type=self.src_vol_type['name'],
+ snapshot_id=snapshot['id'])
+
+ # Delete the snapshot
+ self.snapshots_client.delete_snapshot(snapshot['id'])
+ self.snapshots_client.wait_for_resource_deletion(snapshot['id'])
+
+ # Migrate the volume from snapshot to the second backend
+ self._retype_volume(src_vol)
diff --git a/tempest/api/volume/test_volumes_backup.py b/tempest/api/volume/test_volumes_backup.py
index 552b231..07cfad5 100644
--- a/tempest/api/volume/test_volumes_backup.py
+++ b/tempest/api/volume/test_volumes_backup.py
@@ -128,7 +128,7 @@
volume_details = self.volumes_client.show_volume(
volume['id'])['volume']
- self.assertEqual('true', volume_details['bootable'])
+ self.assertTrue(volume_details['bootable'])
# Create a backup
backup = self.create_backup(volume_id=volume['id'])
@@ -140,7 +140,7 @@
restored_volume_info = self.volumes_client.show_volume(
restored_volume_id)['volume']
- self.assertEqual('true', restored_volume_info['bootable'])
+ self.assertTrue(restored_volume_info['bootable'])
class VolumesBackupsV39Test(base.BaseVolumeTest):
diff --git a/tempest/api/volume/test_volumes_extend.py b/tempest/api/volume/test_volumes_extend.py
index 362f4cc..5d339c4 100644
--- a/tempest/api/volume/test_volumes_extend.py
+++ b/tempest/api/volume/test_volumes_extend.py
@@ -44,7 +44,6 @@
@decorators.idempotent_id('86be1cba-2640-11e5-9c82-635fb964c912')
@testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
"Cinder volume snapshots are disabled")
- @decorators.skip_because(bug='1687044')
def test_volume_extend_when_volume_has_snapshot(self):
volume = self.create_volume()
self.create_snapshot(volume['id'])
diff --git a/tempest/clients.py b/tempest/clients.py
index d75a712..707127c 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -199,6 +199,8 @@
self.catalog_client = self.identity_v3.CatalogClient(**params_v3)
self.project_tags_client = self.identity_v3.ProjectTagsClient(
**params_v3)
+ self.application_credentials_client = \
+ self.identity_v3.ApplicationCredentialsClient(**params_v3)
# Token clients do not use the catalog. They only need default_params.
# They read auth_url, so they should only be set if the corresponding
@@ -269,6 +271,7 @@
# Set default client for users that don't need explicit version
self.volumes_client_latest = self.volumes_v2_client
self.snapshots_client_latest = self.snapshots_v2_client
+ self.backups_client_latest = self.backups_v2_client
if CONF.volume_feature_enabled.api_v3:
self.backups_v3_client = self.volume_v3.BackupsClient()
@@ -284,6 +287,7 @@
# Set default client for users that don't need explicit version
self.volumes_client_latest = self.volumes_v3_client
self.snapshots_client_latest = self.snapshots_v3_client
+ self.backups_client_latest = self.backups_v3_client
def _set_object_storage_clients(self):
self.account_client = self.object_storage.AccountClient()
diff --git a/tempest/cmd/init.py b/tempest/cmd/init.py
index 9a85d89..84c8631 100644
--- a/tempest/cmd/init.py
+++ b/tempest/cmd/init.py
@@ -136,7 +136,7 @@
if not os.path.isdir(local_dir):
LOG.debug('Creating local working dir: %s', local_dir)
os.mkdir(local_dir)
- elif not os.listdir(local_dir) == []:
+ elif os.listdir(local_dir):
raise OSError("Directory you are trying to initialize already "
"exists and is not empty: %s" % local_dir)
diff --git a/tempest/config.py b/tempest/config.py
index a2ccb84..008c9f6 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -240,7 +240,13 @@
'settings enabled?'),
cfg.BoolOpt('project_tags',
default=False,
- help='Is the project tags identity v3 API available?')
+ help='Is the project tags identity v3 API available?'),
+ # Application credentials is a default feature in Queens. This config
+ # option can removed once Pike is EOL.
+ cfg.BoolOpt('application_credentials',
+ default=False,
+ help='Does the environment have application credentials '
+ 'enabled?')
]
compute_group = cfg.OptGroup(name='compute',
@@ -1072,7 +1078,7 @@
return opt_list
-# this should never be called outside of this class
+# This should never be called outside of this module
class TempestConfigPrivate(object):
"""Provides OpenStack configuration information."""
diff --git a/tempest/hacking/ignored_list_T110.txt b/tempest/hacking/ignored_list_T110.txt
deleted file mode 100644
index 0e7e894..0000000
--- a/tempest/hacking/ignored_list_T110.txt
+++ /dev/null
@@ -1 +0,0 @@
-./tempest/services/object_storage/object_client.py
diff --git a/tempest/lib/api_schema/response/compute/v2_19/servers.py b/tempest/lib/api_schema/response/compute/v2_19/servers.py
index 05cc32c..fd9e933 100644
--- a/tempest/lib/api_schema/response/compute/v2_19/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_19/servers.py
@@ -14,9 +14,9 @@
import copy
-from tempest.lib.api_schema.response.compute.v2_1 import servers as serversv21
from tempest.lib.api_schema.response.compute.v2_16 import servers \
as serversv216
+from tempest.lib.api_schema.response.compute.v2_9 import servers as serversv29
list_servers = copy.deepcopy(serversv216.list_servers)
@@ -32,20 +32,20 @@
list_servers_detail['response_body']['properties']['servers']['items'][
'required'].append('description')
-update_server = copy.deepcopy(serversv21.update_server)
+update_server = copy.deepcopy(serversv29.update_server)
update_server['response_body']['properties']['server'][
'properties'].update({'description': {'type': ['string', 'null']}})
update_server['response_body']['properties']['server'][
'required'].append('description')
-rebuild_server = copy.deepcopy(serversv21.rebuild_server)
+rebuild_server = copy.deepcopy(serversv29.rebuild_server)
rebuild_server['response_body']['properties']['server'][
'properties'].update({'description': {'type': ['string', 'null']}})
rebuild_server['response_body']['properties']['server'][
'required'].append('description')
rebuild_server_with_admin_pass = copy.deepcopy(
- serversv21.rebuild_server_with_admin_pass)
+ serversv29.rebuild_server_with_admin_pass)
rebuild_server_with_admin_pass['response_body']['properties']['server'][
'properties'].update({'description': {'type': ['string', 'null']}})
rebuild_server_with_admin_pass['response_body']['properties']['server'][
diff --git a/tempest/lib/api_schema/response/compute/v2_26/servers.py b/tempest/lib/api_schema/response/compute/v2_26/servers.py
index b03bdf6..5c35eab 100644
--- a/tempest/lib/api_schema/response/compute/v2_26/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_26/servers.py
@@ -43,6 +43,25 @@
list_servers_detail['response_body']['properties']['servers']['items'][
'required'].append('tags')
+update_server = copy.deepcopy(servers219.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'tags': tag_items})
+update_server['response_body']['properties']['server'][
+ 'required'].append('tags')
+
+rebuild_server = copy.deepcopy(servers219.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'tags': tag_items})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('tags')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers219.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'tags': tag_items})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('tags')
+
# list response schema wasn't changed for v2.26 so use v2.1
list_servers = copy.deepcopy(servers21.list_servers)
diff --git a/tempest/lib/api_schema/response/compute/v2_47/servers.py b/tempest/lib/api_schema/response/compute/v2_47/servers.py
index 37a084f..935be70 100644
--- a/tempest/lib/api_schema/response/compute/v2_47/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_47/servers.py
@@ -37,3 +37,19 @@
get_server = copy.deepcopy(servers226.get_server)
get_server['response_body']['properties']['server'][
'properties'].update({'flavor': flavor})
+list_servers_detail = copy.deepcopy(servers226.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'properties'].update({'flavor': flavor})
+
+update_server = copy.deepcopy(servers226.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'flavor': flavor})
+
+rebuild_server = copy.deepcopy(servers226.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'flavor': flavor})
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers226.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'flavor': flavor})
diff --git a/tempest/lib/api_schema/response/compute/v2_9/servers.py b/tempest/lib/api_schema/response/compute/v2_9/servers.py
index e260e48..7df02d5 100644
--- a/tempest/lib/api_schema/response/compute/v2_9/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_9/servers.py
@@ -14,6 +14,7 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import servers as servers_21
from tempest.lib.api_schema.response.compute.v2_6 import servers
list_servers = copy.deepcopy(servers.list_servers)
@@ -29,3 +30,22 @@
'properties'].update({'locked': {'type': 'boolean'}})
list_servers_detail['response_body']['properties']['servers']['items'][
'required'].append('locked')
+
+update_server = copy.deepcopy(servers_21.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+update_server['response_body']['properties']['server'][
+ 'required'].append('locked')
+
+rebuild_server = copy.deepcopy(servers_21.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('locked')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers_21.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('locked')
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index 09bccab..c85af1f 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -156,11 +156,11 @@
url = 'servers'
schema = self.get_schema(self.schema_versions_info)
- _schema = schema.list_servers
-
if detail:
url += '/detail'
_schema = schema.list_servers_detail
+ else:
+ _schema = schema.list_servers
if params:
url += '?%s' % urllib.urlencode(params)
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index f302455..da1c51c 100644
--- a/tempest/lib/services/identity/v3/__init__.py
+++ b/tempest/lib/services/identity/v3/__init__.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations under
# the License.
+from tempest.lib.services.identity.v3.application_credentials_client import \
+ ApplicationCredentialsClient
from tempest.lib.services.identity.v3.catalog_client import \
CatalogClient
from tempest.lib.services.identity.v3.credentials_client import \
@@ -46,11 +48,11 @@
from tempest.lib.services.identity.v3.users_client import UsersClient
from tempest.lib.services.identity.v3.versions_client import VersionsClient
-__all__ = ['CatalogClient', 'CredentialsClient', 'DomainsClient',
- 'DomainConfigurationClient', 'EndPointGroupsClient',
- 'EndPointsClient', 'EndPointsFilterClient', 'GroupsClient',
- 'IdentityClient', 'InheritedRolesClient', 'OAUTHConsumerClient',
- 'OAUTHTokenClient', 'PoliciesClient', 'ProjectsClient',
- 'ProjectTagsClient', 'RegionsClient', 'RoleAssignmentsClient',
- 'RolesClient', 'ServicesClient', 'V3TokenClient', 'TrustsClient',
- 'UsersClient', 'VersionsClient']
+__all__ = ['ApplicationCredentialsClient', 'CatalogClient',
+ 'CredentialsClient', 'DomainsClient', 'DomainConfigurationClient',
+ 'EndPointGroupsClient', 'EndPointsClient', 'EndPointsFilterClient',
+ 'GroupsClient', 'IdentityClient', 'InheritedRolesClient',
+ 'OAUTHConsumerClient', 'OAUTHTokenClient', 'PoliciesClient',
+ 'ProjectsClient', 'ProjectTagsClient', 'RegionsClient',
+ 'RoleAssignmentsClient', 'RolesClient', 'ServicesClient',
+ 'V3TokenClient', 'TrustsClient', 'UsersClient', 'VersionsClient']
diff --git a/tempest/lib/services/identity/v3/application_credentials_client.py b/tempest/lib/services/identity/v3/application_credentials_client.py
new file mode 100644
index 0000000..557aa9e
--- /dev/null
+++ b/tempest/lib/services/identity/v3/application_credentials_client.py
@@ -0,0 +1,83 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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.
+
+"""
+https://developer.openstack.org/api-ref/identity/v3/index.html#application-credentials
+"""
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+
+
+class ApplicationCredentialsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def create_application_credential(self, user_id, **kwargs):
+ """Creates an application credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/index.html#create-application-credential
+ """
+ post_body = json.dumps({'application_credential': kwargs})
+ resp, body = self.post('users/%s/application_credentials' % user_id,
+ post_body)
+ self.expected_success(201, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_application_credential(self, user_id, application_credential_id):
+ """Gets details of an application credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/index.html#show-application-credential-details
+ """
+ resp, body = self.get('users/%s/application_credentials/%s' %
+ (user_id, application_credential_id))
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_application_credentials(self, user_id, **params):
+ """Lists out all of a user's application credentials.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/index.html#list-application-credentials
+ """
+ url = 'users/%s/application_credentials' % user_id
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_application_credential(self, user_id,
+ application_credential_id):
+ """Deletes an application credential.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/index.html#delete-application-credential
+ """
+ resp, body = self.delete('users/%s/application_credentials/%s' %
+ (user_id, application_credential_id))
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py
index 33f6f16..d591f03 100644
--- a/tempest/lib/services/identity/v3/token_client.py
+++ b/tempest/lib/services/identity/v3/token_client.py
@@ -51,7 +51,8 @@
def auth(self, user_id=None, username=None, password=None, project_id=None,
project_name=None, user_domain_id=None, user_domain_name=None,
project_domain_id=None, project_domain_name=None, domain_id=None,
- domain_name=None, token=None):
+ domain_name=None, token=None, app_cred_id=None,
+ app_cred_secret=None):
"""Obtains a token from the authentication service
:param user_id: user id
@@ -109,6 +110,13 @@
if _domain:
id_obj['password']['user']['domain'] = _domain
+ if app_cred_id and app_cred_secret:
+ id_obj['methods'].append('application_credential')
+ id_obj['application_credential'] = {
+ 'id': app_cred_id,
+ 'secret': app_cred_secret,
+ }
+
if (project_id or project_name):
_project = dict()
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index cf53b67..6809f4d 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -83,6 +83,7 @@
if CONF.service_available.cinder:
cls.volumes_client = cls.os_primary.volumes_client_latest
cls.snapshots_client = cls.os_primary.snapshots_client_latest
+ cls.backups_client = cls.os_primary.backups_client_latest
# ## Test functions library
#
@@ -244,6 +245,37 @@
volume = self.volumes_client.show_volume(volume['id'])['volume']
return volume
+ def create_backup(self, volume_id, name=None, description=None,
+ force=False, snapshot_id=None, incremental=False,
+ container=None):
+
+ name = name or data_utils.rand_name(
+ self.__class__.__name__ + "-backup")
+ kwargs = {'name': name,
+ 'description': description,
+ 'force': force,
+ 'snapshot_id': snapshot_id,
+ 'incremental': incremental,
+ 'container': container}
+ backup = self.backups_client.create_backup(volume_id=volume_id,
+ **kwargs)['backup']
+ self.addCleanup(self.backups_client.delete_backup, backup['id'])
+ waiters.wait_for_volume_resource_status(self.backups_client,
+ backup['id'], 'available')
+ return backup
+
+ def restore_backup(self, backup_id):
+ restore = self.backups_client.restore_backup(backup_id)['restore']
+ self.addCleanup(self.volumes_client.delete_volume,
+ restore['volume_id'])
+ waiters.wait_for_volume_resource_status(self.backups_client,
+ backup_id, 'available')
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ restore['volume_id'],
+ 'available')
+ self.assertEqual(backup_id, restore['backup_id'])
+ return restore
+
def create_volume_snapshot(self, volume_id, name=None, description=None,
metadata=None, force=False):
name = name or data_utils.rand_name(
diff --git a/tempest/scenario/test_server_advanced_ops.py b/tempest/scenario/test_server_advanced_ops.py
index 89b9fdd..8aa729b 100644
--- a/tempest/scenario/test_server_advanced_ops.py
+++ b/tempest/scenario/test_server_advanced_ops.py
@@ -32,7 +32,6 @@
"""The test suite for server advanced operations
This test case stresses some advanced server instance operations:
- * Resizing a volume-backed instance
* Sequence suspend resume
"""
diff --git a/tempest/scenario/test_volume_backup_restore.py b/tempest/scenario/test_volume_backup_restore.py
new file mode 100644
index 0000000..c23b564
--- /dev/null
+++ b/tempest/scenario/test_volume_backup_restore.py
@@ -0,0 +1,91 @@
+# Copyright 2018 Red Hat, Inc.
+# 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 import utils
+from tempest import config
+from tempest.lib import decorators
+from tempest.scenario import manager
+
+CONF = config.CONF
+
+
+class TestVolumeBackupRestore(manager.ScenarioTest):
+ """Test cinder backup and restore
+
+ This testcase verifies content preservation after backup and restore
+ operations by booting a server from a restored backup and check the
+ connectivity to it.
+
+ The following is the scenario outline:
+ 1. Create volume from image.
+ 2. Create a backup for the volume.
+ 3. Restore the backup.
+ 4. Boot a server from the restored backup.
+ 5. Create a floating ip.
+ 6. Check server connectivity.
+ """
+
+ @classmethod
+ def skip_checks(cls):
+ super(TestVolumeBackupRestore, cls).skip_checks()
+ if not CONF.volume_feature_enabled.backup:
+ raise cls.skipException('Backup is not enable.')
+
+ @decorators.idempotent_id('2ce5e55c-4085-43c1-98c6-582525334ad7')
+ @decorators.attr(type='slow')
+ @utils.services('compute', 'volume', 'image')
+ def test_volume_backup_restore(self):
+ # Create volume from image
+ img_uuid = CONF.compute.image_ref
+ volume = self.create_volume(imageRef=img_uuid)
+ volume_details = self.volumes_client.show_volume(
+ volume['id'])['volume']
+ self.assertEqual('true', volume_details['bootable'])
+
+ # Create a backup
+ backup = self.create_backup(volume_id=volume['id'])
+
+ # Restore the backup
+ restored_volume_id = self.restore_backup(backup['id'])['volume_id']
+
+ # Verify the restored backup volume is bootable
+ restored_volume_info = self.volumes_client.show_volume(
+ restored_volume_id)['volume']
+ self.assertEqual('true', restored_volume_info['bootable'])
+
+ # Create keypair and security group
+ keypair = self.create_keypair()
+ security_group = self._create_security_group()
+
+ # Boot a server from the restored backup
+ bd_map_v2 = [{
+ 'uuid': restored_volume_id,
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 0}]
+ server = self.create_server(image_id='',
+ block_device_mapping_v2=bd_map_v2,
+ key_name=keypair['name'],
+ security_groups=[
+ {'name': security_group['name']}])
+
+ # Create a floating ip
+ floating_ip = self.create_floating_ip(server)
+
+ # Check server connectivity
+ self.check_vm_connectivity(floating_ip['ip'],
+ username=CONF.validation.image_ssh_user,
+ private_key=keypair['private_key'],
+ should_connect=True)
diff --git a/tempest/tests/api/compute/test_base.py b/tempest/tests/api/compute/test_base.py
index 5024100..47f4ad6 100644
--- a/tempest/tests/api/compute/test_base.py
+++ b/tempest/tests/api/compute/test_base.py
@@ -173,3 +173,20 @@
# make our assertions
wait_for_image_status.assert_called_once_with(
compute_images_client, image_id, 'SAVING')
+
+ def _test_version_compatible(self, max_version, expected=True):
+ actual = (compute_base.BaseV2ComputeTest.
+ is_requested_microversion_compatible(max_version))
+ self.assertEqual(expected, actual)
+
+ def test_check_lower_version(self):
+ compute_base.BaseV2ComputeTest.request_microversion = '2.8'
+ self._test_version_compatible('2.40')
+
+ def test_check_euqal_version(self):
+ compute_base.BaseV2ComputeTest.request_microversion = '2.40'
+ self._test_version_compatible('2.40')
+
+ def test_check_higher_version(self):
+ compute_base.BaseV2ComputeTest.request_microversion = '2.41'
+ self._test_version_compatible('2.40', expected=False)
diff --git a/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
new file mode 100644
index 0000000..9bf9b68
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
@@ -0,0 +1,156 @@
+# Copyright 2018 SUSE Linux GmbH
+#
+# 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.lib.services.identity.v3 import application_credentials_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestApplicationCredentialsClient(base.BaseServiceTest):
+ FAKE_CREATE_APP_CRED = {
+ "application_credential": {
+ "description": "fake application credential",
+ "roles": [
+ {
+ "id": "c60fdd45",
+ "domain_id": None,
+ "name": "Member"
+ }
+ ],
+ "expires_at": "2019-02-27T18:30:59.999999Z",
+ "secret": "_BVq0xU5L",
+ "unrestricted": None,
+ "project_id": "ddef321",
+ "id": "5499a186",
+ "name": "one"
+ }
+ }
+
+ FAKE_LIST_APP_CREDS = {
+ "application_credentials": [
+ {
+ "description": "fake application credential",
+ "roles": [
+ {
+ "domain_id": None,
+ "name": "Member",
+ "id": "c60fdd45",
+ }
+ ],
+ "expires_at": "2018-02-27T18:30:59.999999Z",
+ "unrestricted": None,
+ "project_id": "ddef321",
+ "id": "5499a186",
+ "name": "one"
+ },
+ {
+ "description": None,
+ "roles": [
+ {
+ "id": "0f1837c8",
+ "domain_id": None,
+ "name": "anotherrole"
+ },
+ {
+ "id": "c60fdd45",
+ "domain_id": None,
+ "name": "Member"
+ }
+ ],
+ "expires_at": None,
+ "unrestricted": None,
+ "project_id": "c5403d938",
+ "id": "d441c904f",
+ "name": "two"
+ }
+ ]
+ }
+
+ FAKE_APP_CRED_INFO = {
+ "application_credential": {
+ "description": None,
+ "roles": [
+ {
+ "domain_id": None,
+ "name": "Member",
+ "id": "c60fdd45",
+ }
+ ],
+ "expires_at": None,
+ "unrestricted": None,
+ "project_id": "ddef321",
+ "id": "5499a186",
+ "name": "one"
+ }
+ }
+
+ def setUp(self):
+ super(TestApplicationCredentialsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = \
+ application_credentials_client.ApplicationCredentialsClient(
+ fake_auth, 'identity', 'regionOne')
+
+ def _test_create_app_cred(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_application_credential,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_APP_CRED,
+ bytes_body,
+ status=201,
+ user_id="123456")
+
+ def _test_show_app_cred(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_application_credential,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_APP_CRED_INFO,
+ bytes_body,
+ user_id="123456",
+ application_credential_id="5499a186")
+
+ def _test_list_app_creds(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_application_credentials,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_APP_CREDS,
+ bytes_body,
+ user_id="123456")
+
+ def test_create_application_credential_with_str_body(self):
+ self._test_create_app_cred()
+
+ def test_create_application_credential_with_bytes_body(self):
+ self._test_create_app_cred(bytes_body=True)
+
+ def test_show_application_credential_with_str_body(self):
+ self._test_show_app_cred()
+
+ def test_show_application_credential_with_bytes_body(self):
+ self._test_show_app_cred(bytes_body=True)
+
+ def test_list_application_credential_with_str_body(self):
+ self._test_list_app_creds()
+
+ def test_list_application_credential_with_bytes_body(self):
+ self._test_list_app_creds(bytes_body=True)
+
+ def test_delete_trust(self):
+ self.check_service_client_function(
+ self.client.delete_application_credential,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ user_id="123456",
+ application_credential_id="5499a186",
+ status=204)