Merge "Add common method to check the microversion compatibility"
diff --git a/.zuul.yaml b/.zuul.yaml
index e4e95f2..04d60fe 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$
diff --git a/HACKING.rst b/HACKING.rst
index f961884..bb55ac5 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -363,13 +363,24 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When adding tests for new features that were not in previous releases of the
-projects the new test has to be properly skipped with a feature flag. Whether
-this is just as simple as using the @utils.requires_ext() decorator to
-check if the required extension (or discoverable optional API) is enabled or
+projects the new test has to be properly skipped with a feature flag. This can
+be just as simple as using the ``@utils.requires_ext()`` or
+``testtools.skipUnless`` decorators to check if the required extension (or
+discoverable optional API) or feature is enabled or can be as difficult as
adding a new config option to the appropriate section. If there isn't a method
of selecting the new **feature** from the config file then there won't be a
-mechanism to disable the test with older stable releases and the new test won't
-be able to merge.
+mechanism to disable the test with older stable releases and the new test
+won't be able to merge.
+
+Introduction of a new feature flag requires specifying a default value for
+the corresponding config option that is appropriate in the latest OpenStack
+release. Because Tempest is branchless, the feature flag's default value will
+need to be overridden to a value that is appropriate in earlier releases
+in which the feature isn't available. In DevStack, this can be accomplished
+by modifying Tempest's `lib installation script`_ for previous branches
+(because DevStack is branched).
+
+.. _lib installation script: http://git.openstack.org/cgit/openstack-dev/devstack/tree/lib/tempest
2. Bug fix on core project needing Tempest changes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/README.rst b/README.rst
index f8059ae..044ae09 100644
--- a/README.rst
+++ b/README.rst
@@ -15,7 +15,7 @@
This is a set of integration tests to be run against a live OpenStack
cluster. Tempest has batteries of tests for OpenStack API validation,
-Scenarios, and other specific tests useful in validating an OpenStack
+scenarios, and other specific tests useful in validating an OpenStack
deployment.
Design Principles
diff --git a/REVIEWING.rst b/REVIEWING.rst
index 766d0c6..a880181 100644
--- a/REVIEWING.rst
+++ b/REVIEWING.rst
@@ -42,8 +42,8 @@
API Stability
-------------
-Tests should only be added for a published stable APIs. If a patch contains
-tests for an API which hasn't been marked as stable or for an API that which
+Tests should only be added for published stable APIs. If a patch contains
+tests for an API which hasn't been marked as stable or for an API which
doesn't conform to the `API stability guidelines
<https://wiki.openstack.org/wiki/Governance/Approved/APIStability>`_ then it
should not be approved.
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/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
new file mode 100644
index 0000000..dfbcc7d
--- /dev/null
+++ b/releasenotes/notes/identity-v3-project-tags-client-36683c6a8644e54b.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - |
+ Add ``project_tags_client`` to the identity v3 library. This feature
+ enables the possibility of invoking the following API actions:
+
+ * update_project_tag
+ * list_project_tags
+ * update_all_project_tags
+ * check_project_tag_existence
+ * delete_project_tag
+ * delete_all_project_tags
diff --git a/releasenotes/notes/vnc-hardcoded-server-name-removed-6f8d1e90a175dc08.yaml b/releasenotes/notes/vnc-hardcoded-server-name-removed-6f8d1e90a175dc08.yaml
new file mode 100644
index 0000000..0da2ddc
--- /dev/null
+++ b/releasenotes/notes/vnc-hardcoded-server-name-removed-6f8d1e90a175dc08.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ New string configuration option ``vnc_server_header`` is added
+ to ``compute-feature-enabled`` section. It offers to provide VNC server
+ name that is to be expected in the responce header. For example, obvious
+ at hand names is 'WebSockify', 'nginx'.
+fixes:
+ - |
+ Fix VNC server response header issue when it is behind reverse proxy
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_aggregates_negative.py b/tempest/api/compute/admin/test_aggregates_negative.py
index 6df8410..a6e0efa 100644
--- a/tempest/api/compute/admin/test_aggregates_negative.py
+++ b/tempest/api/compute/admin/test_aggregates_negative.py
@@ -27,15 +27,16 @@
def setup_clients(cls):
super(AggregatesAdminNegativeTestJSON, cls).setup_clients()
cls.client = cls.os_admin.aggregates_client
- cls.hyper_client = cls.os_admin.hypervisor_client
+ cls.services_client = cls.os_admin.services_client
@classmethod
def resource_setup(cls):
super(AggregatesAdminNegativeTestJSON, cls).resource_setup()
cls.aggregate_name_prefix = 'test_aggregate'
- hyper_list = cls.hyper_client.list_hypervisors()['hypervisors']
- cls.hosts = [v['hypervisor_hostname'] for v in hyper_list
+ svc_list = cls.services_client.list_services(
+ binary='nova-compute')['services']
+ cls.hosts = [v['host'] for v in svc_list
if v['status'] == 'enabled' and v['state'] == 'up']
def _create_test_aggregate(self):
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index c27f16d..975728c 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -562,10 +562,10 @@
def get_host_other_than(self, server_id):
source_host = self.get_host_for_server(server_id)
- hypers = self.os_admin.hypervisor_client.list_hypervisors(
- )['hypervisors']
- hosts = [hyper['hypervisor_hostname'] for hyper in hypers
- if hyper['state'] == 'up' and hyper['status'] == 'enabled']
+ svcs = self.os_admin.services_client.list_services(
+ binary='nova-compute')['services']
+ hosts = [svc['host'] for svc in svcs
+ if svc['state'] == 'up' and svc['status'] == 'enabled']
for target_host in hosts:
if source_host != target_host:
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index c660821..122c4f5 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -135,8 +135,13 @@
servers_client=self.client)
hostname = linux_client.exec_command("hostname").rstrip()
msg = ('Failed while verifying servername equals hostname. Expected '
- 'hostname "%s" but got "%s".' % (self.name, hostname))
- self.assertEqual(self.name.lower(), hostname, msg)
+ 'hostname "%s" but got "%s".' %
+ (self.name, hostname.split(".")[0]))
+ # NOTE(zhufl): Some images will add postfix for the hostname, e.g.,
+ # if hostname is "aaa", postfix ".novalocal" may be added to it, and
+ # the hostname will be "aaa.novalocal" then, so we should ignore the
+ # postfix when checking whether hostname equals self.name.
+ self.assertEqual(self.name.lower(), hostname.split(".")[0], msg)
class ServersTestManualDisk(ServersTestJSON):
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index 43046ca..b0d527c 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -47,8 +47,8 @@
raise cls.skipException('Neutron is required')
if not CONF.validation.run_validation:
raise cls.skipException('Validation must be enabled')
- if (not CONF.compute_feature_enabled.config_drive
- and not CONF.compute_feature_enabled.metadata_service):
+ if (not CONF.compute_feature_enabled.config_drive and
+ not CONF.compute_feature_enabled.metadata_service):
raise cls.skipException('One of metadata or config drive must be '
'enabled')
diff --git a/tempest/api/compute/servers/test_novnc.py b/tempest/api/compute/servers/test_novnc.py
index d9581e3..1dfd0f9 100644
--- a/tempest/api/compute/servers/test_novnc.py
+++ b/tempest/api/compute/servers/test_novnc.py
@@ -151,11 +151,22 @@
self.assertTrue(
self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
b'Protocols\r\n'),
- 'Did not get the expected 101 on the websockify call: '
- + six.text_type(self._websocket.response))
- self.assertTrue(
- self._websocket.response.find(b'Server: WebSockify') > 0,
- 'Did not get the expected WebSocket HTTP Response.')
+ 'Did not get the expected 101 on the {} call: {}'.format(
+ CONF.compute_feature_enabled.vnc_server_header,
+ six.text_type(self._websocket.response)
+ )
+ )
+ # Since every other server type returns Headers with different case
+ # (for example 'nginx'), lowercase must be applied to eliminate issues.
+ _desired_header = "server: {0}".format(
+ CONF.compute_feature_enabled.vnc_server_header
+ ).lower()
+ _response = six.text_type(self._websocket.response).lower()
+ self.assertIn(
+ _desired_header,
+ _response,
+ 'Did not get the expected WebSocket HTTP Response.'
+ )
@decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
def test_novnc(self):
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 5c3e9f0..bbec30c 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -586,7 +586,7 @@
server_info = self.client.show_server(self.server_id)['server']
if 'SHELVED' in server_info['status']:
self.client.unshelve_server(self.server_id)
- self.addOnException(_unshelve_server)
+ self.addCleanup(_unshelve_server)
server = self.client.show_server(self.server_id)['server']
image_name = server['name'] + '-shelved'
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index 9b545af..e944c28 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -481,7 +481,7 @@
server_info = self.client.show_server(self.server_id)['server']
if 'SHELVED' in server_info['status']:
self.client.unshelve_server(self.server_id)
- self.addOnException(_unshelve_server)
+ self.addCleanup(_unshelve_server)
server = self.client.show_server(self.server_id)['server']
image_name = server['name'] + '-shelved'
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_project_tags.py b/tempest/api/identity/admin/v3/test_project_tags.py
new file mode 100644
index 0000000..d05173b
--- /dev/null
+++ b/tempest/api/identity/admin/v3/test_project_tags.py
@@ -0,0 +1,66 @@
+# Copyright 2018 AT&T Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import testtools
+
+from tempest.api.identity import base
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+CONF = config.CONF
+
+
+class IdentityV3ProjectTagsTest(base.BaseIdentityV3AdminTest):
+
+ @decorators.idempotent_id('7c123aac-999d-416a-a0fb-84b915ab10de')
+ @testtools.skipUnless(CONF.identity_feature_enabled.project_tags,
+ 'Project tags not available.')
+ def test_list_update_delete_project_tags(self):
+ project = self.setup_test_project()
+
+ # Create a tag for testing.
+ tag = data_utils.rand_name('tag')
+ # NOTE(felipemonteiro): The response body for create is empty.
+ self.project_tags_client.update_project_tag(project['id'], tag)
+
+ # Verify that the tag was created.
+ self.project_tags_client.check_project_tag_existence(
+ project['id'], tag)
+
+ # Verify that updating the project tags works.
+ tags_to_update = [data_utils.rand_name('tag') for _ in range(3)]
+ updated_tags = self.project_tags_client.update_all_project_tags(
+ project['id'], tags_to_update)['tags']
+ self.assertEqual(sorted(tags_to_update), sorted(updated_tags))
+
+ # Verify that listing project tags works.
+ retrieved_tags = self.project_tags_client.list_project_tags(
+ project['id'])['tags']
+ self.assertEqual(sorted(tags_to_update), sorted(retrieved_tags))
+
+ # Verify that deleting a project tag works.
+ self.project_tags_client.delete_project_tag(
+ project['id'], tags_to_update[0])
+ self.assertRaises(lib_exc.NotFound,
+ self.project_tags_client.check_project_tag_existence,
+ project['id'], tags_to_update[0])
+
+ # Verify that deleting all project tags works.
+ self.project_tags_client.delete_all_project_tags(project['id'])
+ retrieved_tags = self.project_tags_client.list_project_tags(
+ project['id'])['tags']
+ self.assertEmpty(retrieved_tags)
diff --git a/tempest/api/identity/base.py b/tempest/api/identity/base.py
index 9edccbb..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):
@@ -228,6 +230,7 @@
cls.domain_config_client = cls.os_admin.domain_config_client
cls.endpoint_filter_client = cls.os_admin.endpoint_filter_client
cls.endpoint_groups_client = cls.os_admin.endpoint_groups_client
+ cls.project_tags_client = cls.os_admin.project_tags_client
if CONF.identity.admin_domain_scope:
# NOTE(andreaf) When keystone policy requires it, the identity
@@ -288,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/v1/test_images.py b/tempest/api/image/v1/test_images.py
index 76723f4..2432c8b 100644
--- a/tempest/api/image/v1/test_images.py
+++ b/tempest/api/image/v1/test_images.py
@@ -132,8 +132,8 @@
@classmethod
def skip_checks(cls):
super(ListImagesTest, cls).skip_checks()
- if (len(CONF.image.container_formats) < 2
- or len(CONF.image.disk_formats) < 2):
+ if (len(CONF.image.container_formats) < 2 or
+ len(CONF.image.disk_formats) < 2):
skip_msg = ("%s skipped as multiple container formats "
"or disk formats are not available." % cls.__name__)
raise cls.skipException(skip_msg)
@@ -227,8 +227,8 @@
self.assertEqual(image['disk_format'], self.disk_format_alt)
result_set = set(map(lambda x: x['id'], images_list))
self.assertTrue(self.same_disk_format_set <= result_set)
- self.assertFalse(self.created_set - self.same_disk_format_set
- <= result_set)
+ self.assertFalse(self.created_set - self.same_disk_format_set <=
+ result_set)
@decorators.idempotent_id('2143655d-96d9-4bec-9188-8674206b4b3b')
def test_index_container_format(self):
@@ -238,8 +238,8 @@
self.assertEqual(image['container_format'], self.container_format)
result_set = set(map(lambda x: x['id'], images_list))
self.assertTrue(self.same_container_format_set <= result_set)
- self.assertFalse(self.created_set - self.same_container_format_set
- <= result_set)
+ self.assertFalse(self.created_set - self.same_container_format_set <=
+ result_set)
@decorators.idempotent_id('feb32ac6-22bb-4a16-afd8-9454bb714b14')
def test_index_max_size(self):
diff --git a/tempest/api/volume/admin/test_volume_hosts.py b/tempest/api/volume/admin/test_volume_hosts.py
index ce0cbd2..7e53ce8 100644
--- a/tempest/api/volume/admin/test_volume_hosts.py
+++ b/tempest/api/volume/admin/test_volume_hosts.py
@@ -46,8 +46,8 @@
# show host API should fail (return code: 404). The cinder-volume host
# is presented in format: <host-name>@driver-name.
c_vol_hosts = [host['host_name'] for host in hosts
- if (host['service'] == 'cinder-volume'
- and host['service-state'] == 'enabled')]
+ if (host['service'] == 'cinder-volume' and
+ host['service-state'] == 'enabled')]
self.assertNotEmpty(c_vol_hosts,
"No available cinder-volume host is found, "
"all hosts that found are: %s" % hosts)
diff --git a/tempest/api/volume/test_volumes_extend.py b/tempest/api/volume/test_volumes_extend.py
index 54052ae..5d339c4 100644
--- a/tempest/api/volume/test_volumes_extend.py
+++ b/tempest/api/volume/test_volumes_extend.py
@@ -32,7 +32,7 @@
@decorators.idempotent_id('9a36df71-a257-43a5-9555-dc7c88e66e0e')
def test_volume_extend(self):
# Extend Volume Test.
- volume = self.create_volume()
+ volume = self.create_volume(image_ref=self.image_ref)
extend_size = volume['size'] + 1
self.volumes_client.extend_volume(volume['id'],
new_size=extend_size)
@@ -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/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index dcd3518..52114bc 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -41,16 +41,19 @@
def test_snapshot_create_delete_with_volume_in_use(self):
# Create a test instance
server = self.create_server()
- self.attach_volume(server['id'], self.volume_origin['id'])
+ # NOTE(zhufl) Here we create volume from self.image_ref for adding
+ # coverage for "creating snapshot from non-blank volume".
+ volume = self.create_volume(image_ref=self.image_ref)
+ self.attach_volume(server['id'], volume['id'])
# Snapshot a volume which attached to an instance with force=False
self.assertRaises(lib_exc.BadRequest, self.create_snapshot,
- self.volume_origin['id'], force=False)
+ volume['id'], force=False)
# Snapshot a volume attached to an instance
- snapshot1 = self.create_snapshot(self.volume_origin['id'], force=True)
- snapshot2 = self.create_snapshot(self.volume_origin['id'], force=True)
- snapshot3 = self.create_snapshot(self.volume_origin['id'], force=True)
+ snapshot1 = self.create_snapshot(volume['id'], force=True)
+ snapshot2 = self.create_snapshot(volume['id'], force=True)
+ snapshot3 = self.create_snapshot(volume['id'], force=True)
# Delete the snapshots. Some snapshot implementations can take
# different paths according to order they are deleted.
diff --git a/tempest/api/volume/test_volumes_snapshots_list.py b/tempest/api/volume/test_volumes_snapshots_list.py
index 507df1f..f12bfd8 100644
--- a/tempest/api/volume/test_volumes_snapshots_list.py
+++ b/tempest/api/volume/test_volumes_snapshots_list.py
@@ -28,13 +28,11 @@
@classmethod
def resource_setup(cls):
super(VolumesSnapshotListTestJSON, cls).resource_setup()
- cls.snapshot_id_list = []
volume_origin = cls.create_volume()
# Create snapshots with params
for _ in range(3):
snapshot = cls.create_snapshot(volume_origin['id'])
- cls.snapshot_id_list.append(snapshot['id'])
cls.snapshot = snapshot
def _list_by_param_values_and_assert(self, with_detail=False, **params):
@@ -151,10 +149,14 @@
@decorators.idempotent_id('05489dde-44bc-4961-a1f5-3ce7ee7824f7')
def test_snapshot_list_param_marker(self):
# The list of snapshots should end before the provided marker
- params = {'marker': self.snapshot_id_list[1]}
+ snap_list = self.snapshots_client.list_snapshots()['snapshots']
+ # list_snapshots will take the reverse order as they are created.
+ snapshot_id_list = [snap['id'] for snap in snap_list][::-1]
+
+ params = {'marker': snapshot_id_list[1]}
snap_list = self.snapshots_client.list_snapshots(**params)['snapshots']
fetched_list_id = [snap['id'] for snap in snap_list]
# Verify the list of snapshots ends before the provided
# marker(second snapshot), therefore only the first snapshot
# should displayed.
- self.assertEqual(self.snapshot_id_list[:1], fetched_list_id)
+ self.assertEqual(snapshot_id_list[:1], fetched_list_id)
diff --git a/tempest/clients.py b/tempest/clients.py
index b06eafb..0d16748 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -197,6 +197,10 @@
self.endpoint_groups_client = self.identity_v3.EndPointGroupsClient(
**params_v3)
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
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index 025959a..27e1bc1 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -104,11 +104,11 @@
self.tenant_filter['tenant_id'] = self.tenant_id
def _filter_by_tenant_id(self, item_list):
- if (item_list is None
- or not item_list
- or not hasattr(self, 'tenant_id')
- or self.tenant_id is None
- or 'tenant_id' not in item_list[0]):
+ if (item_list is None or
+ not item_list or
+ not hasattr(self, 'tenant_id') or
+ self.tenant_id is None or
+ 'tenant_id' not in item_list[0]):
return item_list
return [item for item in item_list
@@ -816,8 +816,8 @@
if not self.is_save_state:
roles = [role for role in roles if
(role['id'] not in
- self.saved_state_json['roles'].keys()
- and role['name'] != CONF.identity.admin_role)]
+ self.saved_state_json['roles'].keys() and
+ role['name'] != CONF.identity.admin_role)]
LOG.debug("List count, %s Roles after reconcile", len(roles))
return roles
except Exception:
@@ -852,13 +852,16 @@
def list(self):
projects = self.client.list_projects()['projects']
if not self.is_save_state:
- projects = [project for project in projects if (project['id']
- not in self.saved_state_json['projects'].keys()
- and project['name'] != CONF.auth.admin_project_name)]
+ project_ids = self.saved_state_json['projects']
+ projects = [project
+ for project in projects
+ if (project['id'] not in project_ids and
+ project['name'] != CONF.auth.admin_project_name)]
if self.is_preserve:
- projects = [project for project in projects if project['name']
- not in CONF_PROJECTS]
+ projects = [project
+ for project in projects
+ if project['name'] not in CONF_PROJECTS]
LOG.debug("List count, %s Projects after reconcile", len(projects))
return projects
diff --git a/tempest/config.py b/tempest/config.py
index 5d27efd..7133b3d 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',
@@ -407,6 +413,10 @@
default=False,
help='Enable VNC console. This configuration value should '
'be same as [nova.vnc]->vnc_enabled in nova.conf'),
+ cfg.StrOpt('vnc_server_header',
+ default='WebSockify',
+ help='Expected VNC server name (WebSockify, nginx, etc) '
+ 'in response header.'),
cfg.BoolOpt('spice_console',
default=False,
help='Enable Spice console. This configuration value should '
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index aae685c..b6e7f8c 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -228,12 +228,12 @@
if 'tempest/lib/' not in filename:
return
- if not ('from tempest' in logical_line
- or 'import tempest' in logical_line):
+ if not ('from tempest' in logical_line or
+ 'import tempest' in logical_line):
return
- if ('from tempest.lib' in logical_line
- or 'import tempest.lib' in logical_line):
+ if ('from tempest.lib' in logical_line or
+ 'import tempest.lib' in logical_line):
return
msg = ("T112: tempest.lib should not import local tempest code to avoid "
@@ -266,9 +266,9 @@
if 'tempest/lib/' not in filename:
return
- if ('tempest.config' in logical_line
- or 'from tempest import config' in logical_line
- or 'oslo_config' in logical_line):
+ if ('tempest.config' in logical_line or
+ 'from tempest import config' in logical_line or
+ 'oslo_config' in logical_line):
msg = ('T114: tempest.lib can not have any dependency on tempest '
'config.')
yield(0, msg)
diff --git a/tempest/lib/base.py b/tempest/lib/base.py
index 33a32ee..3be55c0 100644
--- a/tempest/lib/base.py
+++ b/tempest/lib/base.py
@@ -43,8 +43,7 @@
super(BaseTestCase, self).setUp()
if not self.setUpClassCalled:
raise RuntimeError("setUpClass does not calls the super's "
- "setUpClass in the "
- + self.__class__.__name__)
+ "setUpClass in {!r}".format(type(self)))
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
try:
test_timeout = int(test_timeout)
@@ -62,7 +61,7 @@
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
if (os.environ.get('OS_LOG_CAPTURE') != 'False' and
- os.environ.get('OS_LOG_CAPTURE') != '0'):
+ os.environ.get('OS_LOG_CAPTURE') != '0'):
self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
format=self.log_format,
level=None))
diff --git a/tempest/lib/cli/output_parser.py b/tempest/lib/cli/output_parser.py
index 2edd5c1..a7d5e49 100644
--- a/tempest/lib/cli/output_parser.py
+++ b/tempest/lib/cli/output_parser.py
@@ -37,8 +37,8 @@
items = []
tables_ = tables(output_lines)
for table_ in tables_:
- if ('Property' not in table_['headers']
- or 'Value' not in table_['headers']):
+ if ('Property' not in table_['headers'] or
+ 'Value' not in table_['headers']):
raise exceptions.InvalidStructure()
item = {}
for value in table_['values']:
diff --git a/tempest/lib/cmd/check_uuid.py b/tempest/lib/cmd/check_uuid.py
index 101d692..d1f0888 100755
--- a/tempest/lib/cmd/check_uuid.py
+++ b/tempest/lib/cmd/check_uuid.py
@@ -103,7 +103,7 @@
def _modules_search(self):
"""Recursive search for python modules in base package"""
modules = []
- for root, dirs, files in os.walk(self.base_path):
+ for root, _, files in os.walk(self.base_path):
if not os.path.exists(os.path.join(root, '__init__.py')):
continue
root_package = self._path_to_package(root)
@@ -121,10 +121,10 @@
idempotent_id = None
for decorator in test_node.decorator_list:
if (hasattr(decorator, 'func') and
- hasattr(decorator.func, 'attr') and
- decorator.func.attr == DECORATOR_NAME and
- hasattr(decorator.func, 'value') and
- decorator.func.value.id == DECORATOR_MODULE):
+ hasattr(decorator.func, 'attr') and
+ decorator.func.attr == DECORATOR_NAME and
+ hasattr(decorator.func, 'value') and
+ decorator.func.value.id == DECORATOR_MODULE):
for arg in decorator.args:
idempotent_id = ast.literal_eval(arg)
return idempotent_id
@@ -165,8 +165,8 @@
@staticmethod
def _is_test_method(node):
- return (node.__class__ is ast.FunctionDef
- and node.name.startswith('test_'))
+ return (node.__class__ is ast.FunctionDef and
+ node.name.startswith('test_'))
@staticmethod
def _next_node(body, node):
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index 4f1a883..f27e926 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -338,15 +338,15 @@
credentials = self._create_creds(roles=credential_type)
self._creds[str(credential_type)] = credentials
# Maintained until tests are ported
- LOG.info("Acquired dynamic creds:\n credentials: %s", credentials)
- if (self.neutron_available and
- self.create_networks):
+ LOG.info("Acquired dynamic creds:\n"
+ " credentials: %s", credentials)
+ if (self.neutron_available and self.create_networks):
network, subnet, router = self._create_network_resources(
credentials.tenant_id)
credentials.set_resources(network=network, subnet=subnet,
router=router)
- LOG.info("Created isolated network resources for : \n"
- + " credentials: %s", credentials)
+ LOG.info("Created isolated network resources for:\n"
+ " credentials: %s", credentials)
return credentials
def get_primary_creds(self):
diff --git a/tempest/lib/services/identity/v3/__init__.py b/tempest/lib/services/identity/v3/__init__.py
index a539d08..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 \
@@ -33,6 +35,8 @@
from tempest.lib.services.identity.v3.oauth_token_client import \
OAUTHTokenClient
from tempest.lib.services.identity.v3.policies_client import PoliciesClient
+from tempest.lib.services.identity.v3.project_tags_client import \
+ ProjectTagsClient
from tempest.lib.services.identity.v3.projects_client import ProjectsClient
from tempest.lib.services.identity.v3.regions_client import RegionsClient
from tempest.lib.services.identity.v3.role_assignments_client import \
@@ -44,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',
- '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/project_tags_client.py b/tempest/lib/services/identity/v3/project_tags_client.py
new file mode 100644
index 0000000..dd1a2a5
--- /dev/null
+++ b/tempest/lib/services/identity/v3/project_tags_client.py
@@ -0,0 +1,80 @@
+# Copyright 2018 AT&T Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+
+
+class ProjectTagsClient(rest_client.RestClient):
+ api_version = "v3"
+
+ def update_project_tag(self, project_id, tag):
+ """Updates the specified tag and adds it to the project's list of tags.
+
+ """
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.put(url, '{}')
+ # NOTE(felipemonteiro): This API endpoint returns 201 AND an empty
+ # response body, which is consistent with the spec:
+ # https://specs.openstack.org/openstack/api-wg/guidelines/tags.html#addressing-individual-tags
+ self.expected_success(201, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def list_project_tags(self, project_id):
+ """List tags for a project."""
+ url = "projects/%s/tags" % project_id
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_all_project_tags(self, project_id, tags, **kwargs):
+ """Updates all the tags for a project.
+
+ Any existing tags not specified will be deleted.
+
+ For a full list of available parameters, please refer to the official
+ API reference:
+ https://developer.openstack.org/api-ref/identity/v3/#modify-tag-list-for-a-project
+ """
+ body = {'tags': tags}
+ if kwargs:
+ body.update(kwargs)
+ put_body = json.dumps(body)
+ resp, body = self.put('projects/%s/tags' % project_id, put_body)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def check_project_tag_existence(self, project_id, tag):
+ """Check if a project contains a tag."""
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.get(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_project_tag(self, project_id, tag):
+ """Delete a project tag."""
+ url = 'projects/%s/tags/%s' % (project_id, tag)
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_all_project_tags(self, project_id):
+ """Delete all tags from a project."""
+ resp, body = self.delete('projects/%s/tags' % project_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 c78646f..cf53b67 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -662,8 +662,8 @@
addresses = (server['addresses'][network['name']]
if network else [])
for address in addresses:
- if (address['version'] == CONF.validation.ip_version_for_ssh
- and address['OS-EXT-IPS:type'] == 'fixed'):
+ if (address['version'] == CONF.validation.ip_version_for_ssh and # noqa
+ address['OS-EXT-IPS:type'] == 'fixed'):
return address['addr']
raise exceptions.ServerUnreachable(server_id=server['id'])
else:
@@ -792,8 +792,8 @@
port_map = [(p["id"], fxip["ip_address"])
for p in ports
for fxip in p["fixed_ips"]
- if netutils.is_valid_ipv4(fxip["ip_address"])
- and p['status'] in p_status]
+ if (netutils.is_valid_ipv4(fxip["ip_address"]) and
+ p['status'] in p_status)]
inactive = [p for p in ports if p['status'] != 'ACTIVE']
if inactive:
LOG.warning("Instance has ports that are not ACTIVE: %s", inactive)
diff --git a/tempest/scenario/test_aggregates_basic_ops.py b/tempest/scenario/test_aggregates_basic_ops.py
index f762995..b515639 100644
--- a/tempest/scenario/test_aggregates_basic_ops.py
+++ b/tempest/scenario/test_aggregates_basic_ops.py
@@ -37,7 +37,7 @@
super(TestAggregatesBasicOps, cls).setup_clients()
# Use admin client by default
cls.aggregates_client = cls.os_admin.aggregates_client
- cls.hyper_client = cls.os_admin.hypervisor_client
+ cls.services_client = cls.os_admin.services_client
def _create_aggregate(self, **kwargs):
aggregate = (self.aggregates_client.create_aggregate(**kwargs)
@@ -51,9 +51,10 @@
return aggregate
def _get_host_name(self):
- hyper_list = self.hyper_client.list_hypervisors()['hypervisors']
- self.assertNotEmpty(hyper_list)
- return hyper_list[0]['hypervisor_hostname']
+ svc_list = self.services_client.list_services(
+ binary='nova-compute')['services']
+ self.assertNotEmpty(svc_list)
+ return svc_list[0]['host']
def _add_host(self, aggregate_id, host):
aggregate = (self.aggregates_client.add_host(aggregate_id, host=host)
diff --git a/tempest/scenario/test_network_advanced_server_ops.py b/tempest/scenario/test_network_advanced_server_ops.py
index e4ab11c..87ce951 100644
--- a/tempest/scenario/test_network_advanced_server_ops.py
+++ b/tempest/scenario/test_network_advanced_server_ops.py
@@ -43,8 +43,8 @@
@classmethod
def skip_checks(cls):
super(TestNetworkAdvancedServerOps, cls).skip_checks()
- if not (CONF.network.project_networks_reachable
- or CONF.network.public_network_id):
+ if not (CONF.network.project_networks_reachable or
+ CONF.network.public_network_id):
msg = ('Either project_networks_reachable must be "true", or '
'public_network_id must be defined.')
raise cls.skipException(msg)
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index fd9c985..bcd4ddb 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -81,8 +81,8 @@
@classmethod
def skip_checks(cls):
super(TestNetworkBasicOps, cls).skip_checks()
- if not (CONF.network.project_networks_reachable
- or CONF.network.public_network_id):
+ if not (CONF.network.project_networks_reachable or
+ CONF.network.public_network_id):
msg = ('Either project_networks_reachable must be "true", or '
'public_network_id must be defined.')
raise cls.skipException(msg)
diff --git a/tempest/scenario/test_network_v6.py b/tempest/scenario/test_network_v6.py
index 9f4e62b..e4e39c3 100644
--- a/tempest/scenario/test_network_v6.py
+++ b/tempest/scenario/test_network_v6.py
@@ -38,11 +38,11 @@
@classmethod
def skip_checks(cls):
super(TestGettingAddress, cls).skip_checks()
- if not (CONF.network_feature_enabled.ipv6
- and CONF.network_feature_enabled.ipv6_subnet_attributes):
+ if not (CONF.network_feature_enabled.ipv6 and
+ CONF.network_feature_enabled.ipv6_subnet_attributes):
raise cls.skipException('IPv6 or its attributes not supported')
- if not (CONF.network.project_networks_reachable
- or CONF.network.public_network_id):
+ if not (CONF.network.project_networks_reachable or
+ CONF.network.public_network_id):
msg = ('Either project_networks_reachable must be "true", or '
'public_network_id must be defined.')
raise cls.skipException(msg)
diff --git a/tempest/scenario/test_volume_boot_pattern.py b/tempest/scenario/test_volume_boot_pattern.py
index 7ceae89..1fc57e7 100644
--- a/tempest/scenario/test_volume_boot_pattern.py
+++ b/tempest/scenario/test_volume_boot_pattern.py
@@ -193,36 +193,57 @@
@testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
'Cinder volume snapshots are disabled')
@utils.services('compute', 'volume', 'image')
- def test_create_ebs_image_and_check_boot(self):
- # create an instance from volume
+ def test_image_defined_boot_from_volume(self):
+ # create an instance from image-backed volume
volume_origin = self._create_volume_from_image()
instance = self._boot_instance_from_resource(
source_id=volume_origin['id'],
source_type='volume',
delete_on_termination=True)
- # create EBS image
+ # Create a snapshot image from the volume-backed server.
+ # The compute service will have the block service create a snapshot of
+ # the root volume and store its metadata in the image.
image = self.create_server_snapshot(instance)
- # delete instance
+ # Delete the first server which will also delete the first image-backed
+ # volume.
self._delete_server(instance)
- # boot instance from EBS image
+ # Create a server from the image snapshot which has an
+ # "image-defined block device mapping (BDM)" in it, i.e. the metadata
+ # about the volume snapshot. The compute service will use this to
+ # create a volume from the volume snapshot and use that as the root
+ # disk for the server.
instance = self.create_server(image_id=image['id'])
- # Verify the server was created from the image
- created_volume = instance['os-extended-volumes:volumes_attached']
- self.assertNotEmpty(created_volume, "No volume attachment found.")
- created_volume_info = self.volumes_client.show_volume(
- created_volume[0]['id'])['volume']
+ # Verify the server was created from the image-defined BDM.
+ volume_attachments = instance['os-extended-volumes:volumes_attached']
+ self.assertEqual(1, len(volume_attachments),
+ "No volume attachment found.")
+ created_volume = self.volumes_client.show_volume(
+ volume_attachments[0]['id'])['volume']
+ # Assert that the volume service also shows the server attachment.
+ self.assertEqual(1, len(created_volume['attachments']),
+ "No server attachment found for volume: %s" %
+ created_volume)
self.assertEqual(instance['id'],
- created_volume_info['attachments'][0]['server_id'])
- self.assertEqual(created_volume[0]['id'],
- created_volume_info['attachments'][0]['volume_id'])
+ created_volume['attachments'][0]['server_id'])
+ self.assertEqual(volume_attachments[0]['id'],
+ created_volume['attachments'][0]['volume_id'])
self.assertEqual(
volume_origin['volume_image_metadata']['image_id'],
- created_volume_info['volume_image_metadata']['image_id'])
+ created_volume['volume_image_metadata']['image_id'])
- # delete instance
+ # Delete the second server which should also delete the second volume
+ # created from the volume snapshot.
+ # TODO(mriedem): Currently, the compute service fails to delete the
+ # volume it created because the volume still has the snapshot
+ # associated with it, and the cleanups for the volume snapshot which
+ # were added in create_server_snapshot above, don't run until after
+ # this is called. So we need to delete the volume snapshot and wait for
+ # it to be gone here first before deleting the server, and then we can
+ # also assert that the underlying volume is deleted when the server is
+ # deleted.
self._delete_server(instance)
@decorators.idempotent_id('cb78919a-e553-4bab-b73b-10cf4d2eb125')
diff --git a/tempest/test.py b/tempest/test.py
index 27e0165..f2babbb 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -68,9 +68,9 @@
def validate_tearDownClass():
if at_exit_set:
LOG.error(
- "tearDownClass does not call the super's "
- "tearDownClass in these classes: \n"
- + str(at_exit_set))
+ "tearDownClass does not call the super's tearDownClass in "
+ "these classes:\n"
+ " %s", at_exit_set)
atexit.register(validate_tearDownClass)
@@ -582,8 +582,8 @@
super(BaseTestCase, self).setUp()
if not self.__setupclass_called:
raise RuntimeError("setUpClass does not calls the super's"
- "setUpClass in the "
- + self.__class__.__name__)
+ "setUpClass in the " +
+ self.__class__.__name__)
at_exit_set.add(self.__class__)
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
try:
@@ -602,7 +602,7 @@
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
if (os.environ.get('OS_LOG_CAPTURE') != 'False' and
- os.environ.get('OS_LOG_CAPTURE') != '0'):
+ os.environ.get('OS_LOG_CAPTURE') != '0'):
self.useFixture(fixtures.LoggerFixture(nuke_handlers=False,
format=self.log_format,
level=None))
diff --git a/tempest/tests/base.py b/tempest/tests/base.py
index ca81d4d..0b53b45 100644
--- a/tempest/tests/base.py
+++ b/tempest/tests/base.py
@@ -18,7 +18,7 @@
class TestCase(base.BaseTestCase):
- def patch(self, target, **kwargs):
+ def patch(self, target, *args, **kwargs):
"""Returns a started `mock.patch` object for the supplied target.
The caller may then call the returned patcher to create a mock object.
@@ -27,23 +27,35 @@
patcher object, as this method automatically adds a cleanup
to the test class to stop the patcher.
- :param target: String module.class or module.object expression to patch
- :param **kwargs: Passed as-is to `mock.patch`. See mock documentation
- for details.
+ :param target: string module.class or module.object expression to patch
+ :param *args: passed as-is to `mock.patch`.
+ :param **kwargs: passed as-is to `mock.patch`.
+
+ See mock documentation for more details:
+ https://docs.python.org/3.5/library/unittest.mock.html#unittest.mock.patch
"""
- p = mock.patch(target, **kwargs)
+
+ p = mock.patch(target, *args, **kwargs)
m = p.start()
self.addCleanup(p.stop)
return m
- def patchobject(self, target, attribute, new=mock.DEFAULT):
+ def patchobject(self, target, attribute, *args, **kwargs):
"""Convenient wrapper around `mock.patch.object`
Returns a started mock that will be automatically stopped after the
test ran.
+
+ :param target: object to have the attribute patched
+ :param attribute: name of the attribute to be patched
+ :param *args: passed as-is to `mock.patch.object`.
+ :param **kwargs: passed as-is to `mock.patch.object`.
+
+ See mock documentation for more details:
+ https://docs.python.org/3.5/library/unittest.mock.html#unittest.mock.patch.object
"""
- p = mock.patch.object(target, attribute, new)
+ p = mock.patch.object(target, attribute, *args, **kwargs)
m = p.start()
self.addCleanup(p.stop)
return m
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)
diff --git a/tempest/tests/lib/services/identity/v3/test_project_tags_client.py b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
new file mode 100644
index 0000000..2d65a29
--- /dev/null
+++ b/tempest/tests/lib/services/identity/v3/test_project_tags_client.py
@@ -0,0 +1,104 @@
+# Copyright 2018 AT&T Corporation.
+#
+# 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 project_tags_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestProjectTagsClient(base.BaseServiceTest):
+
+ FAKE_PROJECT_ID = "0c4e939acacf4376bdcd1129f1a054ad"
+
+ FAKE_PROJECT_TAG = "foo"
+
+ FAKE_PROJECT_TAGS = ["foo", "bar"]
+
+ def setUp(self):
+ super(TestProjectTagsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = project_tags_client.ProjectTagsClient(fake_auth,
+ 'identity',
+ 'regionOne')
+
+ def _test_update_project_tag(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_project_tag,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=201)
+
+ def _test_list_project_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {"tags": self.FAKE_PROJECT_TAGS},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID)
+
+ def _test_update_all_project_tags(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.update_all_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ {"tags": self.FAKE_PROJECT_TAGS},
+ bytes_body,
+ project_id=self.FAKE_PROJECT_ID,
+ tags=self.FAKE_PROJECT_TAGS)
+
+ def test_update_project_tag_with_str_body(self):
+ self._test_update_project_tag()
+
+ def test_update_project_tag_with_bytes_body(self):
+ self._test_update_project_tag(bytes_body=True)
+
+ def test_list_project_tags_with_str_body(self):
+ self._test_list_project_tags()
+
+ def test_list_project_tags_with_bytes_body(self):
+ self._test_list_project_tags(bytes_body=True)
+
+ def test_update_all_project_tags_with_str_body(self):
+ self._test_update_all_project_tags()
+
+ def test_update_all_project_tags_with_bytes_body(self):
+ self._test_update_all_project_tags(bytes_body=True)
+
+ def test_check_project_project_tag_existence(self):
+ self.check_service_client_function(
+ self.client.check_project_tag_existence,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=204)
+
+ def test_delete_project_tag(self):
+ self.check_service_client_function(
+ self.client.delete_project_tag,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ tag=self.FAKE_PROJECT_TAG,
+ status=204)
+
+ def test_delete_all_project_tags(self):
+ self.check_service_client_function(
+ self.client.delete_all_project_tags,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {},
+ project_id=self.FAKE_PROJECT_ID,
+ status=204)
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index dd05438..bbb9019 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -75,11 +75,13 @@
# json library won't choke.
projects = sorted(filter(is_in_openstack_namespace, json.loads(r.read()[4:])))
-# Retrieve projects having no deb, ui or spec namespace as those namespaces
-# do not contains tempest plugins.
-projects_list = [i for i in projects if not (i.startswith('openstack/deb-') or
- i.endswith('-ui') or
- i.endswith('-specs'))]
+# Retrieve projects having no deb, puppet, ui or spec namespace as those
+# namespaces do not contains tempest plugins.
+projects_list = [i for i in projects if not (
+ i.startswith('openstack/deb-') or
+ i.startswith('openstack/puppet-') or
+ i.endswith('-ui') or
+ i.endswith('-specs'))]
found_plugins = list(filter(has_tempest_plugin, projects_list))