Merge "Use stable constraint in tox to release new tag for Ussuri"
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index a89ad94..2300763 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -1,17 +1,19 @@
-If you would like to contribute to the development of OpenStack, you must
-follow the steps in this page:
+The source repository for this project can be found at:
- https://docs.openstack.org/infra/manual/developers.html
+ https://opendev.org/openstack/tempest
-If you already have a good understanding of how the system works and your
-OpenStack accounts are set up, you can skip to the development workflow
-section of this documentation to learn how changes to OpenStack should be
-submitted for review via the Gerrit tool:
+Pull requests submitted through GitHub are not monitored.
- https://docs.openstack.org/infra/manual/developers.html#development-workflow
+To start contributing to OpenStack, follow the steps in the contribution guide
+to set up and use Gerrit:
-Pull requests submitted through GitHub will be ignored.
+ https://docs.openstack.org/contributors/code-and-documentation/quick-start.html
-Bugs should be filed on Launchpad, not GitHub:
+Bugs should be filed on Launchpad:
https://bugs.launchpad.net/tempest
+
+For more specific information about contributing to this repository, see the
+Tempest contributor guide:
+
+ https://docs.openstack.org/tempest/latest/contributor/contributing.html
diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst
new file mode 100644
index 0000000..38673a0
--- /dev/null
+++ b/doc/source/contributor/contributing.rst
@@ -0,0 +1,59 @@
+============================
+So You Want to Contribute...
+============================
+
+For general information on contributing to OpenStack, please check out the
+`contributor guide <https://docs.openstack.org/contributors/>`_ to get started.
+It covers all the basics that are common to all OpenStack projects: the accounts
+you need, the basics of interacting with our Gerrit review system, how we
+communicate as a community, etc.
+
+Below will cover the more project specific information you need to get started
+with Tempest.
+
+Communication
+~~~~~~~~~~~~~
+* IRC channel #openstack-qa at FreeNode
+* Mailing list (prefix subjects with ``[qa]`` for faster responses)
+ http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss
+
+Contacting the Core Team
+~~~~~~~~~~~~~~~~~~~~~~~~
+Please refer the `Tempest Core Team
+<https://review.opendev.org/#/admin/groups/42,members>`_ contacts.
+
+New Feature Planning
+~~~~~~~~~~~~~~~~~~~~
+If you want to propose a new feature please read `Feature Proposal Process`_
+Tempest features are tracked on `Launchpad BP <https://blueprints.launchpad.net/tempest>`_.
+
+Task Tracking
+~~~~~~~~~~~~~
+We track our tasks in `Launchpad <https://bugs.launchpad.net/tempest>`_.
+
+If you're looking for some smaller, easier work item to pick up and get started
+on, search for the 'low-hanging-fruit' tag.
+
+Reporting a Bug
+~~~~~~~~~~~~~~~
+You found an issue and want to make sure we are aware of it? You can do so on
+`Launchpad <https://bugs.launchpad.net/tempest/+filebug>`__.
+More info about Launchpad usage can be found on `OpenStack docs page
+<https://docs.openstack.org/contributors/common/task-tracking.html#launchpad>`_
+
+Getting Your Patch Merged
+~~~~~~~~~~~~~~~~~~~~~~~~~
+All changes proposed to the Tempest requires two ``Code-Review +2`` votes from
+Tempest core reviewers before one of the core reviewers can approve patch by
+giving ``Workflow +1`` vote. More detailed guidelines for reviewers are available
+at :doc:`../REVIEWING`.
+
+Project Team Lead Duties
+~~~~~~~~~~~~~~~~~~~~~~~~
+All common PTL duties are enumerated in the `PTL guide
+<https://docs.openstack.org/project-team-guide/ptl.html>`_.
+
+Release Process fir QA is documented in `QA Release Process
+<https://wiki.openstack.org/wiki/QA#Releases_Process>`_.
+
+.. _Feature Proposal Process: https://wiki.openstack.org/wiki/QA#Feature_Proposal_.26_Design_discussions
diff --git a/doc/source/index.rst b/doc/source/index.rst
index ab994d1..93fcc3a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -56,6 +56,16 @@
supported_version
+For Contributors
+================
+
+* If you are a new contributor to Tempest please refer: :doc:`contributor/contributing`
+
+.. toctree::
+ :hidden:
+
+ contributor/contributing
+
Developers Guide
================
@@ -80,6 +90,14 @@
plugin
plugin-registry
+Tempest & Plugins Compatible Version Policy
+-------------------------------------------
+
+.. toctree::
+ :maxdepth: 2
+
+ tempest_and_plugins_compatible_version_policy
+
Library
-------
@@ -104,4 +122,4 @@
* :ref:`Tempest document search <search>`: Search the contents of this document.
* `OpenStack wide search <https://docs.openstack.org>`_: Search the wider
- set of OpenStack documentation, including forums.
\ No newline at end of file
+ set of OpenStack documentation, including forums.
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index b4f06e3..50fd4f2 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -414,6 +414,10 @@
.. _2.71: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id64
+ * `2.73`_
+
+ .. _2.73: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id66
+
* Volume
* `3.3`_
diff --git a/doc/source/tempest_and_plugins_compatible_version_policy.rst b/doc/source/tempest_and_plugins_compatible_version_policy.rst
new file mode 100644
index 0000000..942b1bd
--- /dev/null
+++ b/doc/source/tempest_and_plugins_compatible_version_policy.rst
@@ -0,0 +1,54 @@
+Tempest and Plugins compatible version policy
+=============================================
+
+Tempest and its plugins are responsible for the integrated
+testing of OpenStack. These tools have two use cases:
+
+#. Testing upstream code at gate
+#. Testing Production Cloud
+
+Upstream code is tested by the master version of branchless Tempest & plugins
+for all supported stable branches in `Maintained phase`_.
+
+Production Cloud can be tested by using the compatible version or using
+master version. It depends on the testing strategy of cloud. To provide
+the compatible version of Tempest and its Plugins per OpenStack release,
+we started the coordinated release of all plugins and Tempest per OpenStack
+release.
+These versions are the first set of versions from Tempest and its Plugins to
+officially start the support of a particular OpenStack release. For example:
+OpenStack Train release first compatible versions `Tempest plugins version`_.
+
+Because of branchless nature of Tempest and its plugins, first version
+released during OpenStack release is not the last version to support that
+OpenStack release. This means the next (or master) versions can also be used
+for upstream testing as well as in production testing.
+
+Since the `Extended Maintenance policy`_ for stable branch, Tempest
+started releasing the ``end of support`` version once stable release
+is moved to EM state, which used to happen on EOL of stable release. This is
+the last compatible version of Tempest for the OpenStack release moved to EM.
+
+Because of branchless nature as explained above, we have a range of versions
+which can be considered a compatible version for particular OpenStack release.
+How we should release those versions is mentioned in the below table.
+
+ +-----------------------------+-----------------+------------------------------------+
+ | First compatible version -> | OpenStack 'XYZ' | <- Last compatible version |
+ +=============================+=================+====================================+
+ |This is the latest version | |This is the version released |
+ |released when OpenStack | |when OpenStack 'XYZ' is moved to |
+ |'XYZ' is released. | |EM state. Hash used for this should |
+ |Example: | |be the hash from master at the time |
+ |`Tempest plugins version`_ | |of branch is EM not the one used for|
+ | | |First compatible version |
+ +-----------------------------+-----------------+------------------------------------+
+
+Tempest & the Plugins should follow the above mentioned policy for the
+``First compatible version`` and the ``Last compatible version.``
+so that we provide the right set of compatible versions to Upstream as well as to
+Production Cloud testing.
+
+.. _Maintained phase: https://docs.openstack.org/project-team-guide/stable-branches.html#maintained
+.. _Extended Maintenance policy: https://governance.openstack.org/tc/resolutions/20180301-stable-branch-eol.html
+.. _Tempest plugins version: https://releases.openstack.org/train/#tempest-plugins
diff --git a/releasenotes/notes/add-worker-file-option-d949121a61156968.yaml b/releasenotes/notes/add-worker-file-option-d949121a61156968.yaml
new file mode 100644
index 0000000..6b10937
--- /dev/null
+++ b/releasenotes/notes/add-worker-file-option-d949121a61156968.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ Add the option --worker-file in ``tempest run`` command. This is to give
+ tempest more granularity to manually configure how the different sets of
+ tests can be grouped to run with the different worker. You can configure
+ tests regex to run under workers. You can also mix manual scheduling with
+ standard one by mentioning concurrency.
+ For example, the user can setup tempest to run with different concurrences,
+ to be used with different regexps.
diff --git a/releasenotes/notes/tempest-ussuri-release-72b5770a3b97678f.yaml b/releasenotes/notes/tempest-ussuri-release-72b5770a3b97678f.yaml
new file mode 100644
index 0000000..37e56bb
--- /dev/null
+++ b/releasenotes/notes/tempest-ussuri-release-72b5770a3b97678f.yaml
@@ -0,0 +1,16 @@
+---
+prelude: >
+ This release is to tag the Tempest for OpenStack Ussuri release.
+ This release marks the start of Ussuri release support in Tempest.
+ After this release, Tempest will support below OpenStack Releases:
+
+ * Ussuri
+ * Train
+ * Stein
+
+ Current development of Tempest is for OpenStack Victoria development
+ cycle. Every Tempest commit is also tested against master during
+ the Victoria cycle. However, this does not necessarily mean that using
+ Tempest as of this tag will work against a Ussuri (or future release)
+ cloud.
+ To be on safe side, use this tag to test the OpenStack Ussuri release.
diff --git a/tempest/api/compute/admin/test_flavors.py b/tempest/api/compute/admin/test_flavors.py
index 1483c2e..f42f53a 100644
--- a/tempest/api/compute/admin/test_flavors.py
+++ b/tempest/api/compute/admin/test_flavors.py
@@ -46,6 +46,7 @@
@decorators.idempotent_id('8b4330e1-12c4-4554-9390-e6639971f086')
def test_create_flavor_with_int_id(self):
+ """Test creating flavor with id of type integer"""
flavor_id = data_utils.rand_int_id(start=1000)
new_flavor_id = self.create_flavor(ram=self.ram,
vcpus=self.vcpus,
@@ -55,6 +56,7 @@
@decorators.idempotent_id('94c9bb4e-2c2a-4f3c-bb1f-5f0daf918e6d')
def test_create_flavor_with_uuid_id(self):
+ """Test creating flavor with id of type uuid"""
flavor_id = data_utils.rand_uuid()
new_flavor_id = self.create_flavor(ram=self.ram,
vcpus=self.vcpus,
@@ -64,8 +66,11 @@
@decorators.idempotent_id('f83fe669-6758-448a-a85e-32d351f36fe0')
def test_create_flavor_with_none_id(self):
- # If nova receives a request with None as flavor_id,
- # nova generates flavor_id of uuid.
+ """Test creating flavor without id specified
+
+ If nova receives a request with None as flavor_id,
+ nova generates flavor_id of uuid.
+ """
flavor_id = None
new_flavor_id = self.create_flavor(ram=self.ram,
vcpus=self.vcpus,
@@ -75,8 +80,10 @@
@decorators.idempotent_id('8261d7b0-be58-43ec-a2e5-300573c3f6c5')
def test_create_flavor_verify_entry_in_list_details(self):
- # Create a flavor and ensure it's details are listed
- # This operation requires the user to have 'admin' role
+ """Create a flavor and ensure its details are listed
+
+ This operation requires the user to have 'admin' role
+ """
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
# Create the flavor
@@ -94,9 +101,10 @@
@decorators.idempotent_id('63dc64e6-2e79-4fdf-868f-85500d308d66')
def test_create_list_flavor_without_extra_data(self):
- # Create a flavor and ensure it is listed
- # This operation requires the user to have 'admin' role
+ """Create a flavor and ensure it is listed
+ This operation requires the user to have 'admin' role
+ """
def verify_flavor_response_extension(flavor):
# check some extensions for the flavor create/show/detail response
self.assertEqual(flavor['swap'], '')
@@ -134,10 +142,12 @@
@decorators.idempotent_id('be6cc18c-7c5d-48c0-ac16-17eaf03c54eb')
def test_list_non_public_flavor(self):
- # Create a flavor with os-flavor-access:is_public false.
- # The flavor should not be present in list_details as the
- # tenant is not automatically added access list.
- # This operation requires the user to have 'admin' role
+ """Create a flavor with os-flavor-access:is_public false.
+
+ The flavor should not be present in list_details as the
+ tenant is not automatically added access list.
+ This operation requires the user to have 'admin' role
+ """
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
# Create the flavor
@@ -156,7 +166,7 @@
@decorators.idempotent_id('bcc418ef-799b-47cc-baa1-ce01368b8987')
def test_create_server_with_non_public_flavor(self):
- # Create a flavor with os-flavor-access:is_public false
+ """Create a flavor with os-flavor-access:is_public false"""
flavor = self.create_flavor(ram=self.ram, vcpus=self.vcpus,
disk=self.disk,
is_public="False")
@@ -169,8 +179,10 @@
@decorators.idempotent_id('b345b196-bfbd-4231-8ac1-6d7fe15ff3a3')
def test_list_public_flavor_with_other_user(self):
- # Create a Flavor with public access.
- # Try to List/Get flavor with another user
+ """Create a Flavor with public access.
+
+ Try to List/Get flavor with another user
+ """
flavor_name = data_utils.rand_name(self.flavor_name_prefix)
# Create the flavor
@@ -184,6 +196,7 @@
@decorators.idempotent_id('fb9cbde6-3a0e-41f2-a983-bdb0a823c44e')
def test_is_public_string_variations(self):
+ """Test creating public and non public flavors"""
flavor_name_not_public = data_utils.rand_name(self.flavor_name_prefix)
flavor_name_public = data_utils.rand_name(self.flavor_name_prefix)
@@ -215,6 +228,7 @@
@decorators.idempotent_id('3b541a2e-2ac2-4b42-8b8d-ba6e22fcd4da')
def test_create_flavor_using_string_ram(self):
+ """Test creating flavor with ram of type string"""
new_flavor_id = data_utils.rand_int_id(start=1000)
ram = "1024"
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs.py b/tempest/api/compute/admin/test_flavors_extra_specs.py
index 789965e..4c531b3 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs.py
@@ -61,8 +61,11 @@
@decorators.idempotent_id('0b2f9d4b-1ca2-4b99-bb40-165d4bb94208')
def test_flavor_set_get_update_show_unset_keys(self):
- # Test to SET, GET, UPDATE, SHOW, UNSET flavor extra
- # spec as a user with admin privileges.
+ """Test flavor extra spec operations by admin user
+
+ Test to SET, GET, UPDATE, SHOW, UNSET flavor extra
+ spec as a user with admin privileges.
+ """
# Assigning extra specs values that are to be set
specs = {'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
# SET extra specs to the flavor created in setUp
@@ -100,6 +103,7 @@
@decorators.idempotent_id('a99dad88-ae1c-4fba-aeb4-32f898218bd0')
def test_flavor_non_admin_get_all_keys(self):
+ """Test non admin user getting all flavor extra spec keys"""
specs = {'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
self.admin_flavors_client.set_flavor_extra_spec(self.flavor['id'],
**specs)
@@ -111,6 +115,7 @@
@decorators.idempotent_id('12805a7f-39a3-4042-b989-701d5cad9c90')
def test_flavor_non_admin_get_specific_key(self):
+ """Test non admin user getting specific flavor extra spec key"""
specs = {'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
body = self.admin_flavors_client.set_flavor_extra_spec(
self.flavor['id'], **specs
diff --git a/tempest/api/compute/admin/test_flavors_extra_specs_negative.py b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
index 9f89293..721acca 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs_negative.py
@@ -64,7 +64,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('a00a3b81-5641-45a8-ab2b-4a8ec41e1d7d')
def test_flavor_non_admin_set_keys(self):
- # Test to SET flavor extra spec as a user without admin privileges.
+ """Test to SET flavor extra spec as a user without admin privileges"""
self.assertRaises(lib_exc.Forbidden,
self.flavors_client.set_flavor_extra_spec,
self.flavor['id'],
@@ -73,7 +73,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('1ebf4ef8-759e-48fe-a801-d451d80476fb')
def test_flavor_non_admin_update_specific_key(self):
- # non admin user is not allowed to update flavor extra spec
+ """non admin user is not allowed to update flavor extra spec"""
body = self.admin_flavors_client.set_flavor_extra_spec(
self.flavor['id'],
**{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
@@ -89,6 +89,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('28f12249-27c7-44c1-8810-1f382f316b11')
def test_flavor_non_admin_unset_keys(self):
+ """non admin user is not allowed to unset flavor extra spec"""
self.admin_flavors_client.set_flavor_extra_spec(
self.flavor['id'],
**{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
@@ -102,6 +103,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('440b9f3f-3c7f-4293-a106-0ceda350f8de')
def test_flavor_unset_nonexistent_key(self):
+ """Unsetting non existence flavor extra spec key should fail"""
self.assertRaises(lib_exc.NotFound,
self.admin_flavors_client.unset_flavor_extra_spec,
self.flavor['id'],
@@ -110,6 +112,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('329a7be3-54b2-48be-8052-bf2ce4afd898')
def test_flavor_get_nonexistent_key(self):
+ """Getting non existence flavor extra spec key should fail"""
self.assertRaises(lib_exc.NotFound,
self.flavors_client.show_flavor_extra_spec,
self.flavor['id'],
@@ -118,7 +121,10 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('25b822b8-9f49-44f6-80de-d99f0482e5cb')
def test_flavor_update_mismatch_key(self):
- # the key will be updated should be match the key in the body
+ """Updating unmatched flavor extra spec key should fail
+
+ The key to be updated should match the key in the body
+ """
self.assertRaises(lib_exc.BadRequest,
self.admin_flavors_client.update_flavor_extra_spec,
self.flavor['id'],
@@ -128,7 +134,10 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('f5889590-bf66-41cc-b4b1-6e6370cfd93f')
def test_flavor_update_more_key(self):
- # there should be just one item in the request body
+ """Updating multiple flavor spec keys should fail
+
+ There should be just one item in the request body
+ """
self.assertRaises(lib_exc.BadRequest,
self.admin_flavors_client.update_flavor_extra_spec,
self.flavor['id'],
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 836b975..a845c72 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -30,6 +30,7 @@
class LiveMigrationTestBase(base.BaseV2ComputeAdminTest):
+ """Test live migration operations supported by admin user"""
# These tests don't attempt any SSH validation nor do they use
# floating IPs on the instance, so all we need is a network and
@@ -123,12 +124,14 @@
@decorators.idempotent_id('1dce86b8-eb04-4c03-a9d8-9c1dc3ee0c7b')
def test_live_block_migration(self):
+ """Test live migrating an active server"""
self._test_live_migration()
@decorators.idempotent_id('1e107f21-61b2-4988-8f22-b196e938ab88')
@testtools.skipUnless(CONF.compute_feature_enabled.pause,
'Pause is not available.')
def test_live_block_migration_paused(self):
+ """Test live migrating a paused server"""
self._test_live_migration(state='PAUSED')
@testtools.skipUnless(CONF.compute_feature_enabled.
@@ -137,6 +140,7 @@
@decorators.idempotent_id('5071cf17-3004-4257-ae61-73a84e28badd')
@utils.services('volume')
def test_volume_backed_live_migration(self):
+ """Test live migrating an active server booted from volume"""
self._test_live_migration(volume_backed=True)
@decorators.idempotent_id('e19c0cc6-6720-4ed8-be83-b6603ed5c812')
@@ -148,6 +152,7 @@
'Block Live migration not configured for iSCSI')
@utils.services('volume')
def test_iscsi_volume(self):
+ """Test live migrating a server with volume attached"""
server = self.create_test_server(wait_until="ACTIVE")
server_id = server['id']
target_host = self.get_host_other_than(server_id)
diff --git a/tempest/api/compute/admin/test_migrations.py b/tempest/api/compute/admin/test_migrations.py
index 83f2e61..37f5aec 100644
--- a/tempest/api/compute/admin/test_migrations.py
+++ b/tempest/api/compute/admin/test_migrations.py
@@ -25,6 +25,7 @@
class MigrationsAdminTest(base.BaseV2ComputeAdminTest):
+ """Test migration operations supported by admin user"""
@classmethod
def setup_clients(cls):
@@ -33,14 +34,14 @@
@decorators.idempotent_id('75c0b83d-72a0-4cf8-a153-631e83e7d53f')
def test_list_migrations(self):
- # Admin can get the migrations list
+ """Test admin user can get the migrations list"""
self.client.list_migrations()
@decorators.idempotent_id('1b512062-8093-438e-b47a-37d2f597cd64')
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_list_migrations_in_flavor_resize_situation(self):
- # Admin can get the migrations list which contains the resized server
+ """Admin can get the migrations list containing the resized server"""
server = self.create_test_server(wait_until="ACTIVE")
server_id = server['id']
@@ -62,8 +63,11 @@
@testtools.skipUnless(CONF.compute_feature_enabled.resize,
'Resize not available.')
def test_resize_server_revert_deleted_flavor(self):
- # Tests that we can revert the resize on an instance whose original
- # flavor has been deleted.
+ """Test reverting resized server with original flavor deleted
+
+ Tests that we can revert the resize on an instance whose original
+ flavor has been deleted.
+ """
# First we have to create a flavor that we can delete so make a copy
# of the normal flavor from which we'd create a server.
@@ -137,10 +141,12 @@
@testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
'Cold migration not available.')
def test_cold_migration(self):
+ """Test cold migrating server and then confirm the migration"""
self._test_cold_migrate_server(revert=False)
@decorators.idempotent_id('caa1aa8b-f4ef-4374-be0d-95f001c2ac2d')
@testtools.skipUnless(CONF.compute_feature_enabled.cold_migration,
'Cold migration not available.')
def test_revert_cold_migration(self):
+ """Test cold migrating server and then revert the migration"""
self._test_cold_migrate_server(revert=True)
diff --git a/tempest/api/compute/admin/test_networks.py b/tempest/api/compute/admin/test_networks.py
index 33b23b5..fb6376e 100644
--- a/tempest/api/compute/admin/test_networks.py
+++ b/tempest/api/compute/admin/test_networks.py
@@ -35,6 +35,7 @@
@decorators.idempotent_id('d206d211-8912-486f-86e2-a9d090d1f416')
def test_get_network(self):
+ """Test getting network from nova side"""
networks = self.client.list_networks()['networks']
if CONF.compute.fixed_network_name:
configured_network = [x for x in networks if x['label'] ==
@@ -56,6 +57,7 @@
@decorators.idempotent_id('df3d1046-6fa5-4b2c-ad0c-cfa46a351cb9')
def test_list_all_networks(self):
+ """Test getting all networks from nova side"""
networks = self.client.list_networks()['networks']
# Check the configured network is in the list
if CONF.compute.fixed_network_name:
diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py
index bebc8c5..f440428 100644
--- a/tempest/api/compute/admin/test_servers_on_multinodes.py
+++ b/tempest/api/compute/admin/test_servers_on_multinodes.py
@@ -23,7 +23,7 @@
class ServersOnMultiNodesTest(base.BaseV2ComputeAdminTest):
-
+ """Test creating servers on mutiple nodes with scheduler_hints."""
@classmethod
def resource_setup(cls):
super(ServersOnMultiNodesTest, cls).resource_setup()
@@ -65,6 +65,7 @@
compute.is_scheduler_filter_enabled("SameHostFilter"),
'SameHostFilter is not available.')
def test_create_servers_on_same_host(self):
+ """Test creating servers with hints 'same_host'"""
hints = {'same_host': self.server01}
server02 = self.create_test_server(scheduler_hints=hints,
wait_until='ACTIVE')['id']
@@ -76,6 +77,7 @@
compute.is_scheduler_filter_enabled("DifferentHostFilter"),
'DifferentHostFilter is not available.')
def test_create_servers_on_different_hosts(self):
+ """Test creating servers with hints of single 'different_host'"""
hints = {'different_host': self.server01}
server02 = self.create_test_server(scheduler_hints=hints,
wait_until='ACTIVE')['id']
@@ -87,7 +89,7 @@
compute.is_scheduler_filter_enabled("DifferentHostFilter"),
'DifferentHostFilter is not available.')
def test_create_servers_on_different_hosts_with_list_of_servers(self):
- # This scheduler-hint supports list of servers also.
+ """Test creating servers with hints of a list of 'different_host'"""
hints = {'different_host': [self.server01]}
server02 = self.create_test_server(scheduler_hints=hints,
wait_until='ACTIVE')['id']
diff --git a/tempest/api/compute/admin/test_services_negative.py b/tempest/api/compute/admin/test_services_negative.py
index d264829..033caa8 100644
--- a/tempest/api/compute/admin/test_services_negative.py
+++ b/tempest/api/compute/admin/test_services_negative.py
@@ -31,14 +31,18 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('1126d1f8-266e-485f-a687-adc547492646')
def test_list_services_with_non_admin_user(self):
+ """Non admin user is not allowed to list nova services"""
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.list_services)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('d0884a69-f693-4e79-a9af-232d15643bf7')
def test_get_service_by_invalid_params(self):
- # Expect all services to be returned when the request contains invalid
- # parameters.
+ """Test listing services by invalid filter should return all services
+
+ Expect all services to be returned when the request contains invalid
+ parameters.
+ """
services = self.client.list_services()['services']
services_xxx = (self.client.list_services(xxx='nova-compute')
['services'])
@@ -47,6 +51,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('1e966d4a-226e-47c7-b601-0b18a27add54')
def test_get_service_by_invalid_service_and_valid_host(self):
+ """Test listing services by invalid service and valid host value"""
services = self.client.list_services()['services']
host_name = services[0]['host']
services = self.client.list_services(host=host_name,
@@ -56,6 +61,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('64e7e7fb-69e8-4cb6-a71d-8d5eb0c98655')
def test_get_service_with_valid_service_and_invalid_host(self):
+ """Test listing services by valid service and invalid host value"""
services = self.client.list_services()['services']
binary_name = services[0]['binary']
services = self.client.list_services(host='xxx',
@@ -79,6 +85,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('508671aa-c929-4479-bd10-8680d40dd0a6')
def test_enable_service_with_invalid_service_id(self):
+ """Test updating non existing service to status enabled"""
self.assertRaises(lib_exc.NotFound,
self.client.update_service,
service_id=self.fake_service_id,
@@ -87,6 +94,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('a9eeeade-42b3-419f-87aa-c9342aa068cf')
def test_disable_service_with_invalid_service_id(self):
+ """Test updating non existing service to status disabled"""
self.assertRaises(lib_exc.NotFound,
self.client.update_service,
service_id=self.fake_service_id,
@@ -95,6 +103,8 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('f46a9d91-1e85-4b96-8e7a-db7706fa2e9a')
def test_disable_log_reason_with_invalid_service_id(self):
+ """Test updating non existing service to disabled with reason"""
+
# disabled_reason requires that status='disabled' be provided.
self.assertRaises(lib_exc.NotFound,
self.client.update_service,
diff --git a/tempest/api/compute/admin/test_volumes_negative.py b/tempest/api/compute/admin/test_volumes_negative.py
index 133f4bc..7b0f48b 100644
--- a/tempest/api/compute/admin/test_volumes_negative.py
+++ b/tempest/api/compute/admin/test_volumes_negative.py
@@ -67,10 +67,10 @@
volume_min_microversion = '3.27'
@classmethod
- def skip_checks(self):
- super(UpdateMultiattachVolumeNegativeTest, self).skip_checks()
+ def skip_checks(cls):
+ super(UpdateMultiattachVolumeNegativeTest, cls).skip_checks()
if not CONF.compute_feature_enabled.volume_multiattach:
- raise self.skipException('Volume multi-attach is not available.')
+ raise cls.skipException('Volume multi-attach is not available.')
@decorators.attr(type=['negative'])
@decorators.idempotent_id('7576d497-b7c6-44bd-9cc5-c5b4e50fec71')
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index 1f7eb7b..8879369 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from json import decoder as json_decoder
+
from oslo_log import log as logging
from oslo_serialization import jsonutils as json
@@ -110,7 +112,11 @@
max_microversion = '2.32'
def verify_device_metadata(self, md_json):
- md_dict = json.loads(md_json)
+ try:
+ md_dict = json.loads(md_json)
+ except (json_decoder.JSONDecodeError, TypeError):
+ return False
+
for d in md_dict['devices']:
if d['type'] == 'nic':
if d['mac'] == self.port1['mac_address']:
@@ -310,7 +316,11 @@
raise cls.skipException('Metadata API must be enabled')
def verify_device_metadata(self, md_json):
- md_dict = json.loads(md_json)
+ try:
+ md_dict = json.loads(md_json)
+ except (json_decoder.JSONDecodeError, TypeError):
+ return False
+
found_devices = [d['tags'][0] for d in md_dict['devices']
if d.get('tags')]
try:
diff --git a/tempest/api/compute/servers/test_server_rescue.py b/tempest/api/compute/servers/test_server_rescue.py
index 6629794..1ac9516 100644
--- a/tempest/api/compute/servers/test_server_rescue.py
+++ b/tempest/api/compute/servers/test_server_rescue.py
@@ -103,3 +103,75 @@
# Delete Security group
self.servers_client.remove_security_group(self.rescued_server_id,
name=sg['name'])
+
+
+class ServerStableDeviceRescueTest(base.BaseV2ComputeTest):
+
+ @classmethod
+ def skip_checks(self):
+ super(ServerStableDeviceRescueTest, self).skip_checks()
+ if not CONF.compute_feature_enabled.rescue:
+ msg = "Server rescue not available."
+ raise self.skipException(msg)
+ if not CONF.compute_feature_enabled.stable_rescue:
+ msg = "Stable rescue not available."
+ raise self.skipException(msg)
+
+ def _create_server_and_rescue_image(self, hw_rescue_device=None,
+ hw_rescue_bus=None):
+ server_id = self.create_test_server(wait_until='ACTIVE')['id']
+ image_id = self.create_image_from_server(server_id,
+ wait_until='ACTIVE')['id']
+ if hw_rescue_bus:
+ self.images_client.update_image(
+ image_id, [dict(add='/hw_rescue_bus',
+ value=hw_rescue_bus)])
+ if hw_rescue_device:
+ self.images_client.update_image(
+ image_id, [dict(add='/hw_rescue_device',
+ value=hw_rescue_device)])
+ return server_id, image_id
+
+ def _test_stable_device_rescue(self, server_id, rescue_image_id):
+ self.servers_client.rescue_server(
+ server_id, rescue_image_ref=rescue_image_id)
+ waiters.wait_for_server_status(
+ self.servers_client, server_id, 'RESCUE')
+ self.servers_client.unrescue_server(server_id)
+ waiters.wait_for_server_status(
+ self.servers_client, server_id, 'ACTIVE')
+
+ @decorators.idempotent_id('947004c3-e8ef-47d9-9f00-97b74f9eaf96')
+ def test_stable_device_rescue_cdrom_ide(self):
+ server_id, rescue_image_id = self._create_server_and_rescue_image(
+ hw_rescue_device='cdrom', hw_rescue_bus='ide')
+ self._test_stable_device_rescue(server_id, rescue_image_id)
+
+ @decorators.idempotent_id('16865750-1417-4854-bcf7-496e6753c01e')
+ def test_stable_device_rescue_disk_virtio(self):
+ server_id, rescue_image_id = self._create_server_and_rescue_image(
+ hw_rescue_device='disk', hw_rescue_bus='virtio')
+ self._test_stable_device_rescue(server_id, rescue_image_id)
+
+ @decorators.idempotent_id('12340157-6306-4745-bdda-cfa019908b48')
+ def test_stable_device_rescue_disk_scsi(self):
+ server_id, rescue_image_id = self._create_server_and_rescue_image(
+ hw_rescue_device='disk', hw_rescue_bus='scsi')
+ self._test_stable_device_rescue(server_id, rescue_image_id)
+
+ @decorators.idempotent_id('647d04cf-ad35-4956-89ab-b05c5c16f30c')
+ def test_stable_device_rescue_disk_usb(self):
+ server_id, rescue_image_id = self._create_server_and_rescue_image(
+ hw_rescue_device='disk', hw_rescue_bus='usb')
+ self._test_stable_device_rescue(server_id, rescue_image_id)
+
+ @decorators.idempotent_id('a3772b42-00bf-4310-a90b-1cc6fd3e7eab')
+ def test_stable_device_rescue_disk_virtio_with_volume_attached(self):
+ server_id, rescue_image_id = self._create_server_and_rescue_image(
+ hw_rescue_device='disk', hw_rescue_bus='virtio')
+ server = self.servers_client.show_server(server_id)['server']
+ volume = self.create_volume()
+ self.attach_volume(server, volume)
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ volume['id'], 'in-use')
+ self._test_stable_device_rescue(server_id, rescue_image_id)
diff --git a/tempest/api/identity/admin/v2/test_endpoints.py b/tempest/api/identity/admin/v2/test_endpoints.py
index 947706e..236ce7c 100644
--- a/tempest/api/identity/admin/v2/test_endpoints.py
+++ b/tempest/api/identity/admin/v2/test_endpoints.py
@@ -19,6 +19,7 @@
class EndPointsTestJSON(base.BaseIdentityV2AdminTest):
+ """Test keystone v2 endpoints"""
@classmethod
def resource_setup(cls):
@@ -51,6 +52,7 @@
@decorators.idempotent_id('11f590eb-59d8-4067-8b2b-980c7f387f51')
def test_list_endpoints(self):
+ """Test listing keystone endpoints"""
# Get a list of endpoints
fetched_endpoints = self.endpoints_client.list_endpoints()['endpoints']
# Asserting LIST endpoints
@@ -62,6 +64,7 @@
@decorators.idempotent_id('9974530a-aa28-4362-8403-f06db02b26c1')
def test_create_list_delete_endpoint(self):
+ """Test creating, listing and deleting a keystone endpoint"""
region = data_utils.rand_name('region')
url = data_utils.rand_url()
endpoint = self.endpoints_client.create_endpoint(
diff --git a/tempest/api/identity/admin/v3/test_default_project_id.py b/tempest/api/identity/admin/v3/test_default_project_id.py
index 73fddb7..7c3a6cc 100644
--- a/tempest/api/identity/admin/v3/test_default_project_id.py
+++ b/tempest/api/identity/admin/v3/test_default_project_id.py
@@ -22,6 +22,7 @@
class TestDefaultProjectId(base.BaseIdentityV3AdminTest):
+ """Test creating a token without project will default to user's project"""
@classmethod
def setup_credentials(cls):
@@ -35,11 +36,11 @@
self.domains_client.delete_domain(domain_id)
@testtools.skipIf(CONF.identity_feature_enabled.immutable_user_source,
- 'Skipped because environment has an '
- 'immutable user source and solely '
- 'provides read-only access to users.')
+ 'Skipped because environment has an immutable user '
+ 'source and solely provides read-only access to users.')
@decorators.idempotent_id('d6110661-6a71-49a7-a453-b5e26640ff6d')
def test_default_project_id(self):
+ """Creating a token without project will default to user's project"""
# create a domain
dom_name = data_utils.rand_name('dom')
domain_body = self.domains_client.create_domain(
diff --git a/tempest/api/identity/admin/v3/test_list_users.py b/tempest/api/identity/admin/v3/test_list_users.py
index 5aec931..7bd0bcf 100644
--- a/tempest/api/identity/admin/v3/test_list_users.py
+++ b/tempest/api/identity/admin/v3/test_list_users.py
@@ -22,6 +22,7 @@
class UsersV3TestJSON(base.BaseIdentityV3AdminTest):
+ """Test listing keystone users"""
def _list_users_with_params(self, params, key, expected, not_expected):
# Helper method to list users filtered with params and
@@ -69,7 +70,7 @@
@decorators.idempotent_id('08f9aabb-dcfe-41d0-8172-82b5fa0bd73d')
def test_list_user_domains(self):
- # List users with domain
+ """List users with domain"""
params = {'domain_id': self.domain['id']}
self._list_users_with_params(params, 'domain_id',
self.domain_enabled_user,
@@ -77,7 +78,7 @@
@decorators.idempotent_id('bff8bf2f-9408-4ef5-b63a-753c8c2124eb')
def test_list_users_with_not_enabled(self):
- # List the users with not enabled
+ """List the users with not enabled"""
params = {'enabled': False}
self._list_users_with_params(params, 'enabled',
self.non_domain_enabled_user,
@@ -85,7 +86,7 @@
@decorators.idempotent_id('c285bb37-7325-4c02-bff3-3da5d946d683')
def test_list_users_with_name(self):
- # List users with name
+ """List users with name"""
params = {'name': self.domain_enabled_user['name']}
# When domain specific drivers are enabled the operations
# of listing all users and listing all groups are not supported,
@@ -98,7 +99,7 @@
@decorators.idempotent_id('b30d4651-a2ea-4666-8551-0c0e49692635')
def test_list_users(self):
- # List users
+ """List users"""
# When domain specific drivers are enabled the operations
# of listing all users and listing all groups are not supported,
# they need a domain filter to be specified
@@ -120,7 +121,7 @@
@decorators.idempotent_id('b4baa3ae-ac00-4b4e-9e27-80deaad7771f')
def test_get_user(self):
- # Get a user detail
+ """Get a user detail"""
user = self.users_client.show_user(self.users[0]['id'])['user']
self.assertEqual(self.users[0]['id'], user['id'])
self.assertEqual(self.users[0]['name'], user['name'])
diff --git a/tempest/api/identity/admin/v3/test_policies.py b/tempest/api/identity/admin/v3/test_policies.py
index 2908fc4..fb81d0a 100644
--- a/tempest/api/identity/admin/v3/test_policies.py
+++ b/tempest/api/identity/admin/v3/test_policies.py
@@ -19,13 +19,14 @@
class PoliciesTestJSON(base.BaseIdentityV3AdminTest):
+ """Test keystone policies"""
def _delete_policy(self, policy_id):
self.policies_client.delete_policy(policy_id)
@decorators.idempotent_id('1a0ad286-2d06-4123-ab0d-728893a76201')
def test_list_policies(self):
- # Test to list policies
+ """Test to list keystone policies"""
policy_ids = list()
fetched_ids = list()
for _ in range(3):
@@ -46,7 +47,7 @@
@decorators.attr(type='smoke')
@decorators.idempotent_id('e544703a-2f03-4cf2-9b0f-350782fdb0d3')
def test_create_update_delete_policy(self):
- # Test to update policy
+ """Test to update keystone policy"""
blob = data_utils.rand_name('BlobName')
policy_type = data_utils.rand_name('PolicyType')
policy = self.policies_client.create_policy(blob=blob,
diff --git a/tempest/api/identity/admin/v3/test_project_tags.py b/tempest/api/identity/admin/v3/test_project_tags.py
index b7878a8..eed60af 100644
--- a/tempest/api/identity/admin/v3/test_project_tags.py
+++ b/tempest/api/identity/admin/v3/test_project_tags.py
@@ -25,6 +25,8 @@
class IdentityV3ProjectTagsTest(base.BaseIdentityV3AdminTest):
+ """Test keystone project tags"""
+
# NOTE: force_tenant_isolation is true in the base class by default but
# overridden to false here to allow test execution for clouds using the
# pre-provisioned credentials provider.
@@ -34,6 +36,7 @@
@testtools.skipUnless(CONF.identity_feature_enabled.project_tags,
'Project tags not available.')
def test_list_update_delete_project_tags(self):
+ """Test listing, updating and deleting of project tags"""
project = self.setup_test_project()
# Create a tag for testing.
diff --git a/tempest/api/identity/admin/v3/test_services.py b/tempest/api/identity/admin/v3/test_services.py
index 5afeb98..a649d27 100644
--- a/tempest/api/identity/admin/v3/test_services.py
+++ b/tempest/api/identity/admin/v3/test_services.py
@@ -20,6 +20,7 @@
class ServicesTestJSON(base.BaseIdentityV3AdminTest):
+ """Test keystone services"""
def _del_service(self, service_id):
# Used for deleting the services created in this class
@@ -31,6 +32,7 @@
@decorators.attr(type='smoke')
@decorators.idempotent_id('5193aad5-bcb7-411d-85b0-b3b61b96ef06')
def test_create_update_get_service(self):
+ """Test creating, updating and getting of keystone service"""
# Creating a Service
name = data_utils.rand_name('service')
serv_type = data_utils.rand_name('type')
@@ -63,7 +65,7 @@
@decorators.idempotent_id('d1dcb1a1-2b6b-4da8-bbb8-5532ef6e8269')
def test_create_service_without_description(self):
- # Create a service only with name and type
+ """Create a keystone service only with name and type"""
name = data_utils.rand_name('service')
serv_type = data_utils.rand_name('type')
service = self.services_client.create_service(
@@ -74,7 +76,7 @@
@decorators.idempotent_id('e55908e8-360e-439e-8719-c3230a3e179e')
def test_list_services(self):
- # Create, List, Verify and Delete Services
+ """Create, List, Verify and Delete Keystone Services"""
service_ids = list()
service_types = list()
for _ in range(3):
diff --git a/tempest/api/identity/admin/v3/test_users.py b/tempest/api/identity/admin/v3/test_users.py
index 8955a93..31cbbac 100644
--- a/tempest/api/identity/admin/v3/test_users.py
+++ b/tempest/api/identity/admin/v3/test_users.py
@@ -27,6 +27,7 @@
class UsersV3TestJSON(base.BaseIdentityV3AdminTest):
+ """Test keystone users"""
@classmethod
def skip_checks(cls):
@@ -38,7 +39,7 @@
@decorators.idempotent_id('b537d090-afb9-4519-b95d-270b0708e87e')
def test_user_update(self):
- # Test case to check if updating of user attributes is successful.
+ """Test case to check if updating of user attributes is successful"""
# Creating first user
u_name = data_utils.rand_name('user')
u_desc = u_name + 'description'
@@ -72,6 +73,7 @@
@decorators.idempotent_id('2d223a0e-e457-4a70-9fb1-febe027a0ff9')
def test_update_user_password(self):
+ """Test updating user password"""
# Creating User to check password updation
u_name = data_utils.rand_name('user')
original_password = data_utils.rand_password()
@@ -98,7 +100,7 @@
@decorators.idempotent_id('a831e70c-e35b-430b-92ed-81ebbc5437b8')
def test_list_user_projects(self):
- # List the projects that a user has access upon
+ """Test listing the projects that a user has access upon"""
assigned_project_ids = list()
fetched_project_ids = list()
u_project = self.setup_test_project()
@@ -141,7 +143,7 @@
@decorators.idempotent_id('c10dcd90-461d-4b16-8e23-4eb836c00644')
def test_get_user(self):
- # Get a user detail
+ """Test getting a user detail"""
user = self.setup_test_user()
fetched_user = self.users_client.show_user(user['id'])['user']
self.assertEqual(user['id'], fetched_user['id'])
@@ -150,6 +152,7 @@
'Security compliance not available.')
@decorators.idempotent_id('568cd46c-ee6c-4ab4-a33a-d3791931979e')
def test_password_history_not_enforced_in_admin_reset(self):
+ """Test setting same password when password history is not enforced"""
old_password = self.os_primary.credentials.password
user_id = self.os_primary.credentials.user_id
diff --git a/tempest/api/identity/v3/test_catalog.py b/tempest/api/identity/v3/test_catalog.py
index bc95f0d..ce6adf9 100644
--- a/tempest/api/identity/v3/test_catalog.py
+++ b/tempest/api/identity/v3/test_catalog.py
@@ -19,9 +19,11 @@
class IdentityCatalogTest(base.BaseIdentityV3Test):
+ """Test service's catalog type values"""
@decorators.idempotent_id('56b57ced-22b8-4127-9b8a-565dfb0207e2')
def test_catalog_standardization(self):
+ """Test that every service has a standard catalog type value"""
# https://opendev.org/openstack/service-types-authority
# /src/branch/master/service-types.yaml
standard_service_values = [{'name': 'keystone', 'type': 'identity'},
@@ -31,11 +33,9 @@
# next, we need to GET the catalog using the catalog client
catalog = self.non_admin_catalog_client.show_catalog()['catalog']
# get list of the service types present in the catalog
- catalog_services = []
- for service in catalog:
- catalog_services.append(service['type'])
+ catalog_services = [service['type'] for service in catalog]
for service in standard_service_values:
- # if service enabled, check if it has a standard typevalue
+ # if service enabled, check if it has a standard type value
if service['name'] == 'keystone' or\
getattr(CONF.service_available, service['name']):
self.assertIn(service['type'], catalog_services)
diff --git a/tempest/api/volume/test_versions.py b/tempest/api/volume/test_versions.py
index b602032..1e5c9de 100644
--- a/tempest/api/volume/test_versions.py
+++ b/tempest/api/volume/test_versions.py
@@ -17,12 +17,14 @@
class VersionsTest(base.BaseVolumeTest):
+ """Test cinder versions"""
_api_version = 3
@decorators.idempotent_id('77838fc4-b49b-4c64-9533-166762517369')
@decorators.attr(type='smoke')
def test_list_versions(self):
+ """Test listing cinder versions"""
# NOTE: The version data is checked on service client side
# with JSON-Schema validation. It is enough to just call
# the API here.
@@ -30,6 +32,7 @@
@decorators.idempotent_id('7f755ae2-caa9-4049-988c-331d8f7a579f')
def test_show_version(self):
+ "Test getting cinder version details"
# NOTE: The version data is checked on service client side
# with JSON-Schema validation. So we will loop through each
# version and call show version.
diff --git a/tempest/api/volume/test_volume_delete_cascade.py b/tempest/api/volume/test_volume_delete_cascade.py
index bb32c11..53f1bca 100644
--- a/tempest/api/volume/test_volume_delete_cascade.py
+++ b/tempest/api/volume/test_volume_delete_cascade.py
@@ -58,8 +58,11 @@
@decorators.idempotent_id('994e2d40-de37-46e8-b328-a58fba7e4a95')
def test_volume_delete_cascade(self):
- # The case validates the ability to delete a volume
- # with associated snapshots.
+ """Test deleting a volume with associated snapshots
+
+ The case validates the ability to delete a volume
+ with associated snapshots.
+ """
# Create a volume
volume = self.create_volume()
@@ -78,9 +81,12 @@
@testtools.skipIf(CONF.volume.storage_protocol == 'ceph',
'Skip because of Bug#1677525')
def test_volume_from_snapshot_cascade_delete(self):
- # The case validates the ability to delete a volume with
- # associated snapshot while there is another volume created
- # from that snapshot.
+ """Test deleting a volume with associated volume-associated snapshot
+
+ The case validates the ability to delete a volume with
+ associated snapshot while there is another volume created
+ from that snapshot.
+ """
# Create a volume
volume = self.create_volume()
diff --git a/tempest/api/volume/test_volume_metadata.py b/tempest/api/volume/test_volume_metadata.py
index d203b2d..2151168 100644
--- a/tempest/api/volume/test_volume_metadata.py
+++ b/tempest/api/volume/test_volume_metadata.py
@@ -20,6 +20,7 @@
class VolumesMetadataTest(base.BaseVolumeTest):
+ """Test volume metadata"""
@classmethod
def resource_setup(cls):
@@ -34,6 +35,7 @@
@decorators.idempotent_id('6f5b125b-f664-44bf-910f-751591fe5769')
def test_crud_volume_metadata(self):
+ """Test creating, getting, updating and deleting of volume metadata"""
# Create metadata for the volume
metadata = {"key1": "value1",
"key2": "value2",
@@ -71,6 +73,7 @@
@decorators.idempotent_id('862261c5-8df4-475a-8c21-946e50e36a20')
def test_update_show_volume_metadata_item(self):
+ """Test updating and getting single volume metadata item"""
# Update metadata item for the volume
metadata = {"key1": "value1",
"key2": "value2",
diff --git a/tempest/api/volume/test_volume_transfers.py b/tempest/api/volume/test_volume_transfers.py
index 4cdf898..3eb81f5 100644
--- a/tempest/api/volume/test_volume_transfers.py
+++ b/tempest/api/volume/test_volume_transfers.py
@@ -20,6 +20,7 @@
class VolumesTransfersTest(base.BaseVolumeTest):
+ """Test volume transfer"""
credentials = ['primary', 'alt', 'admin']
@@ -34,6 +35,7 @@
@decorators.idempotent_id('4d75b645-a478-48b1-97c8-503f64242f1a')
def test_create_get_list_accept_volume_transfer(self):
+ """Test creating, getting, listing and accepting of volume transfer"""
# Create a volume first
volume = self.create_volume()
self.addCleanup(self.delete_volume,
@@ -74,6 +76,7 @@
@decorators.idempotent_id('ab526943-b725-4c07-b875-8e8ef87a2c30')
def test_create_list_delete_volume_transfer(self):
+ """Test creating, listing and deleting volume transfer"""
# Create a volume first
volume = self.create_volume()
self.addCleanup(self.delete_volume,
diff --git a/tempest/api/volume/test_volumes_clone.py b/tempest/api/volume/test_volumes_clone.py
index ea39a21..eb54426 100644
--- a/tempest/api/volume/test_volumes_clone.py
+++ b/tempest/api/volume/test_volumes_clone.py
@@ -23,6 +23,7 @@
class VolumesCloneTest(base.BaseVolumeTest):
+ """Test volume clone"""
@classmethod
def skip_checks(cls):
@@ -44,6 +45,7 @@
@decorators.idempotent_id('9adae371-a257-43a5-9555-dc7c88e66e0e')
def test_create_from_volume(self):
+ """Test cloning a volume with increasing size"""
# Creates a volume from another volume passing a size different from
# the source volume.
src_size = CONF.volume.volume_size
@@ -58,6 +60,7 @@
@decorators.idempotent_id('cbbcd7c6-5a6c-481a-97ac-ca55ab715d16')
@utils.services('image')
def test_create_from_bootable_volume(self):
+ """Test cloning a bootable volume"""
# Create volume from image
img_uuid = CONF.compute.image_ref
src_vol = self.create_volume(imageRef=img_uuid)
diff --git a/tempest/api/volume/test_volumes_clone_negative.py b/tempest/api/volume/test_volumes_clone_negative.py
index bba7a0b..4bfb166 100644
--- a/tempest/api/volume/test_volumes_clone_negative.py
+++ b/tempest/api/volume/test_volumes_clone_negative.py
@@ -22,6 +22,7 @@
class VolumesCloneNegativeTest(base.BaseVolumeTest):
+ """Negative tests of volume clone"""
@classmethod
def skip_checks(cls):
@@ -32,6 +33,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('9adae371-a257-43a5-459a-dc7c88e66e0e')
def test_create_from_volume_decreasing_size(self):
+ """Test cloning a volume with decreasing size will fail"""
# Creates a volume from another volume passing a size different from
# the source volume.
src_size = CONF.volume.volume_size + 1
diff --git a/tempest/api/volume/test_volumes_extend.py b/tempest/api/volume/test_volumes_extend.py
index 2267045..041823d 100644
--- a/tempest/api/volume/test_volumes_extend.py
+++ b/tempest/api/volume/test_volumes_extend.py
@@ -28,9 +28,11 @@
class VolumesExtendTest(base.BaseVolumeTest):
+ """Test volume extend"""
@decorators.idempotent_id('9a36df71-a257-43a5-9555-dc7c88e66e0e')
def test_volume_extend(self):
+ """Test extend a volume"""
# Extend Volume Test.
volume = self.create_volume(imageRef=self.image_ref)
extend_size = volume['size'] * 2
@@ -45,6 +47,7 @@
@testtools.skipUnless(CONF.volume_feature_enabled.snapshot,
"Cinder volume snapshots are disabled")
def test_volume_extend_when_volume_has_snapshot(self):
+ """Test extending a volume which has a snapshot"""
volume = self.create_volume()
self.create_snapshot(volume['id'])
diff --git a/tempest/api/volume/test_volumes_get.py b/tempest/api/volume/test_volumes_get.py
index 71db95c..ade2deb 100644
--- a/tempest/api/volume/test_volumes_get.py
+++ b/tempest/api/volume/test_volumes_get.py
@@ -27,6 +27,7 @@
class VolumesGetTest(base.BaseVolumeTest):
+ """Test getting volume info"""
def _volume_create_get_update_delete(self, **kwargs):
# Create a volume, Get it's details and Delete the volume
@@ -118,12 +119,14 @@
@decorators.attr(type='smoke')
@decorators.idempotent_id('27fb0e9f-fb64-41dd-8bdb-1ffa762f0d51')
def test_volume_create_get_update_delete(self):
+ """Test Create/Get/Update/Delete of a blank volume"""
self._volume_create_get_update_delete(size=CONF.volume.volume_size)
@decorators.attr(type='smoke')
@decorators.idempotent_id('54a01030-c7fc-447c-86ee-c1182beae638')
@utils.services('image')
def test_volume_create_get_update_delete_from_image(self):
+ """Test Create/Get/Update/Delete of a volume created from image"""
image = self.images_client.show_image(CONF.compute.image_ref)
min_disk = image['min_disk']
disk_size = max(min_disk, CONF.volume.volume_size)
@@ -134,12 +137,14 @@
@testtools.skipUnless(CONF.volume_feature_enabled.clone,
'Cinder volume clones are disabled')
def test_volume_create_get_update_delete_as_clone(self):
+ """Test Create/Get/Update/Delete of a cloned volume"""
origin = self.create_volume()
self._volume_create_get_update_delete(source_volid=origin['id'],
size=CONF.volume.volume_size)
class VolumesSummaryTest(base.BaseVolumeTest):
+ """Test volume summary"""
_api_version = 3
min_microversion = '3.12'
@@ -147,6 +152,7 @@
@decorators.idempotent_id('c4f2431e-4920-4736-9e00-4040386b6feb')
def test_show_volume_summary(self):
+ """Test showing volume summary"""
volume_summary = \
self.volumes_client.show_volume_summary()['volume-summary']
for key in ['total_size', 'total_count']:
diff --git a/tempest/cmd/run.py b/tempest/cmd/run.py
index f9ca2c7..d82b6df 100644
--- a/tempest/cmd/run.py
+++ b/tempest/cmd/run.py
@@ -47,6 +47,42 @@
by removing unnecessary tests from a list file which is generated from
``--list-tests`` option.
+You can also use ``--worker-file`` option that let you pass a filepath to a
+worker yaml file, allowing you to manually schedule the tests run.
+For example, you can setup a tempest run with
+different concurrences to be used with different regexps.
+An example of worker file is showed below::
+
+ # YAML Worker file
+ - worker:
+ # you can have more than one regex per worker
+ - tempest.api.*
+ - neutron_tempest_tests
+ - worker:
+ - tempest.scenario.*
+
+This will run test matching with 'tempest.api.*' and 'neutron_tempest_tests'
+against worker 1. Run tests matching with 'tempest.scenario.*' under worker 2.
+
+You can mix manual scheduling with the standard scheduling mechanisms by
+concurrency field on a worker. For example::
+
+ # YAML Worker file
+ - worker:
+ # you can have more than one regex per worker
+ - tempest.api.*
+ - neutron_tempest_tests
+ concurrency: 3
+ - worker:
+ - tempest.scenario.*
+ concurrency: 2
+
+This will run tests matching with 'tempest.scenario.*' against 2 workers.
+
+This worker file is passed into stestr. For some more details on how it
+operates please refer to the stestr scheduling docs:
+https://stestr.readthedocs.io/en/stable/MANUAL.html#test-scheduling
+
Test Execution
==============
There are several options to control how the tests are executed. By default
@@ -185,6 +221,7 @@
blacklist_file=parsed_args.blacklist_file,
whitelist_file=parsed_args.whitelist_file,
black_regex=parsed_args.black_regex,
+ worker_path=parsed_args.worker_file,
load_list=parsed_args.load_list, combine=parsed_args.combine)
if return_code > 0:
sys.exit(return_code)
@@ -254,6 +291,10 @@
'on each newline. This command '
'supports files created by the tempest '
'run ``--list-tests`` command')
+ parser.add_argument('--worker-file', '--worker_file',
+ help='Optional path to a worker file. This file '
+ 'contains each worker configuration to be '
+ 'used to schedule the tests run')
# list only args
parser.add_argument('--list-tests', '-l', action='store_true',
help='List tests',
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index cd85ede..edb9d16 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -400,9 +400,24 @@
"""Upgrade the HTTP connection to a WebSocket and verify."""
# It is possible to pass the path as a query parameter in the request,
# so use it if present
+ # Given noVNC format
+ # https://x.com/vnc_auto.html?path=%3Ftoken%3Dxxx,
+ # url format is
+ # ParseResult(scheme='https', netloc='x.com',
+ # path='/vnc_auto.html', params='',
+ # query='path=%3Ftoken%3Dxxx', fragment='').
+ # qparams format is {'path': ['?token=xxx']}
qparams = urlparse.parse_qs(url.query)
- path = qparams['path'][0] if 'path' in qparams else '/websockify'
- reqdata = 'GET %s HTTP/1.1\r\n' % path
+ # according to references
+ # https://docs.python.org/3/library/urllib.parse.html
+ # https://tools.ietf.org/html/rfc3986#section-3.4
+ # qparams['path'][0] format is '?token=xxx' without / prefix
+ # remove / in /websockify to comply to references.
+ path = qparams['path'][0] if 'path' in qparams else 'websockify'
+ # Fix websocket request format by adding / prefix.
+ # Updated request format: GET /?token=xxx HTTP/1.1
+ # or GET /websockify HTTP/1.1
+ reqdata = 'GET /%s HTTP/1.1\r\n' % path
reqdata += 'Host: %s' % url.hostname
# Add port only if we have one specified
if url.port:
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index e242301..14790d6 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -230,7 +230,7 @@
while any(attachment_id == a['attachment_id'] for a in attachments):
time.sleep(client.build_interval)
if int(time.time()) - start >= client.build_timeout:
- message = ('Failed to remove attachment %s from volume %s'
+ message = ('Failed to remove attachment %s from volume %s '
'within the required time (%s s).' %
(attachment_id, volume_id, client.build_timeout))
raise lib_exc.TimeoutException(message)
diff --git a/tempest/config.py b/tempest/config.py
index 9685745..1699c7d 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -521,6 +521,10 @@
default=True,
help='Does the test environment support instance rescue '
'mode?'),
+ cfg.BoolOpt('stable_rescue',
+ default=False,
+ help='Does the test environment support stable device '
+ 'instance rescue mode?'),
cfg.BoolOpt('enable_instance_password',
default=True,
help='Enables returning of the instance password by the '
diff --git a/tempest/lib/api_schema/response/compute/v2_73/__init__.py b/tempest/lib/api_schema/response/compute/v2_73/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_73/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_73/servers.py b/tempest/lib/api_schema/response/compute/v2_73/servers.py
new file mode 100644
index 0000000..6e491e9
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_73/servers.py
@@ -0,0 +1,78 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+
+from tempest.lib.api_schema.response.compute.v2_71 import servers as servers271
+
+
+###########################################################################
+#
+# 2.73:
+#
+# The locked_reason parameter is now returned in the response body of the
+# following calls:
+#
+# - POST /servers/{server_id}/action (where the action is rebuild)
+# - PUT /servers/{server_id} (update)
+# - GET /servers/{server_id} (show)
+# - GET /servers/detail (list)
+#
+###########################################################################
+
+# The "locked_reason" parameter will either be a string or None.
+locked_reason = {'type': ['string', 'null']}
+
+rebuild_server = copy.deepcopy(servers271.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'locked_reason': locked_reason})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('locked_reason')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ servers271.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'locked_reason': locked_reason})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('locked_reason')
+
+update_server = copy.deepcopy(servers271.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'locked_reason': locked_reason})
+update_server['response_body']['properties']['server'][
+ 'required'].append('locked_reason')
+
+get_server = copy.deepcopy(servers271.get_server)
+get_server['response_body']['properties']['server'][
+ 'properties'].update({'locked_reason': locked_reason})
+get_server['response_body']['properties']['server'][
+ 'required'].append('locked_reason')
+
+list_servers_detail = copy.deepcopy(servers271.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'properties'].update({'locked_reason': locked_reason})
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'required'].append('locked_reason')
+
+# NOTE(lajoskatona): Below are the unchanged schema in this microversion. We
+# need to keep this schema in this file to have the generic way to select the
+# right schema based on self.schema_versions_info mapping in service client.
+# ****** Schemas unchanged since microversion 2.71 ***
+list_servers = copy.deepcopy(servers271.list_servers)
+show_server_diagnostics = copy.deepcopy(servers271.show_server_diagnostics)
+get_remote_consoles = copy.deepcopy(servers271.get_remote_consoles)
+list_tags = copy.deepcopy(servers271.list_tags)
+update_all_tags = copy.deepcopy(servers271.update_all_tags)
+delete_all_tags = copy.deepcopy(servers271.delete_all_tags)
+check_tag_existence = copy.deepcopy(servers271.check_tag_existence)
+update_tag = copy.deepcopy(servers271.update_tag)
+delete_tag = copy.deepcopy(servers271.delete_tag)
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index a687137..cbf7a8c 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -35,6 +35,7 @@
from tempest.lib.api_schema.response.compute.v2_63 import servers as schemav263
from tempest.lib.api_schema.response.compute.v2_70 import servers as schemav270
from tempest.lib.api_schema.response.compute.v2_71 import servers as schemav271
+from tempest.lib.api_schema.response.compute.v2_73 import servers as schemav273
from tempest.lib.api_schema.response.compute.v2_8 import servers as schemav28
from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
from tempest.lib.common import rest_client
@@ -59,7 +60,8 @@
{'min': '2.57', 'max': '2.62', 'schema': schemav257},
{'min': '2.63', 'max': '2.69', 'schema': schemav263},
{'min': '2.70', 'max': '2.70', 'schema': schemav270},
- {'min': '2.71', 'max': None, 'schema': schemav271}]
+ {'min': '2.71', 'max': '2.72', 'schema': schemav271},
+ {'min': '2.73', 'max': None, 'schema': schemav273}]
def __init__(self, auth_provider, service, region,
enable_instance_password=True, **kwargs):
diff --git a/tempest/test_discover/plugins.py b/tempest/test_discover/plugins.py
index 7a037eb..b20b60e 100644
--- a/tempest/test_discover/plugins.py
+++ b/tempest/test_discover/plugins.py
@@ -194,11 +194,14 @@
def get_plugin_load_tests_tuple(self):
load_tests_dict = {}
for plug in self.ext_plugins:
+ LOG.info('Loading tests from Tempest plugin: %s', plug.name)
load_tests_dict[plug.name] = plug.obj.load_tests()
return load_tests_dict
def register_plugin_opts(self, conf):
for plug in self.ext_plugins:
+ LOG.info('Register additional config options from Tempest '
+ 'plugin: %s', plug.name)
try:
plug.obj.register_opts(conf)
except Exception:
@@ -209,6 +212,9 @@
plugin_options = []
for plug in self.ext_plugins:
opt_list = plug.obj.get_opt_lists()
+ LOG.info('List additional config options registered by '
+ 'Tempest plugin: %s', plug.name)
+
if opt_list:
plugin_options.extend(opt_list)
return plugin_options
diff --git a/tempest/tests/cmd/test_run.py b/tempest/tests/cmd/test_run.py
index 8997a4c..e9bbcc2 100644
--- a/tempest/tests/cmd/test_run.py
+++ b/tempest/tests/cmd/test_run.py
@@ -153,6 +153,15 @@
result = ["b\'" + x + "\'" for x in result]
self.assertEqual(result, tests)
+ def test_tempest_run_with_worker_file(self):
+ fd, path = tempfile.mkstemp()
+ self.addCleanup(os.remove, path)
+ worker_file = os.fdopen(fd, 'wb', 0)
+ self.addCleanup(worker_file.close)
+ worker_file.write(
+ '- worker:\n - passing\n concurrency: 3'.encode('utf-8'))
+ self.assertRunExit(['tempest', 'run', '--worker-file=%s' % path], 0)
+
def test_tempest_run_with_whitelist(self):
fd, path = tempfile.mkstemp()
self.addCleanup(os.remove, path)