Merge "Catch json.loads exceptions in verify_device_metadata"
diff --git a/.zuul.yaml b/.zuul.yaml
index 70f582e..80d49d8 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -339,6 +339,13 @@
nodeset: openstack-two-node-bionic
# This job runs on Bionic from stable/stein on.
branches: ^(?!stable/(ocata|pike|queens|rocky)).*$
+ vars:
+ devstack_localrc:
+ USE_PYTHON3: False
+ group-vars:
+ subnode:
+ devstack_localrc:
+ USE_PYTHON3: False
- job:
name: tempest-multinode-full
diff --git a/HACKING.rst b/HACKING.rst
index 204b3c7..f8be0ad 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -12,7 +12,6 @@
tempest/scenario tests
- [T104] Scenario tests require a services decorator
- [T105] Tests cannot use setUpClass/tearDownClass
-- [T106] vim configuration should not be kept in source files.
- [T107] Check that a service tag isn't in the module path
- [T108] Check no hyphen at the end of rand_name() argument
- [T109] Cannot use testtools.skip decorator; instead use
diff --git a/doc/source/index.rst b/doc/source/index.rst
index ab994d1..a72e783 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -80,6 +80,14 @@
plugin
plugin-registry
+Tempest & Plugins Compatible Version Policy
+-------------------------------------------
+
+.. toctree::
+ :maxdepth: 2
+
+ tempest_and_plugins_compatible_version_policy
+
Library
-------
@@ -104,4 +112,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-subnet-id-config-option-fac3d6f12abfc171.yaml b/releasenotes/notes/add-subnet-id-config-option-fac3d6f12abfc171.yaml
new file mode 100644
index 0000000..a1bd4c5
--- /dev/null
+++ b/releasenotes/notes/add-subnet-id-config-option-fac3d6f12abfc171.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - A new config option 'subnet_id' is added to section
+ 'network' to specify subnet which should be used for
+ allocation of IPs for VMs created during testing.
+ It should be used when the tested network contains more
+ than one subnet otherwise test of external connectivity
+ will fail. (Fixes bug #1856671)
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/introduce-attachments-client-add-show-attachment-api-c3111f7e560a87b3.yaml b/releasenotes/notes/introduce-attachments-client-add-show-attachment-api-c3111f7e560a87b3.yaml
new file mode 100644
index 0000000..a058137
--- /dev/null
+++ b/releasenotes/notes/introduce-attachments-client-add-show-attachment-api-c3111f7e560a87b3.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ A new attachments client library has been introduced for the volume
+ service.
+
+ Initially only the show_attachment API is provided. This API requires a
+ minimum volume API microversion of ``3.27``.
diff --git a/roles/run-tempest/README.rst b/roles/run-tempest/README.rst
index 1f7fb70..91b0b5f 100644
--- a/roles/run-tempest/README.rst
+++ b/roles/run-tempest/README.rst
@@ -20,14 +20,12 @@
It works only when used with some specific tox environments
('all', 'all-plugin'.)
- Multi-line and commented regexs can be achieved by doing this:
+ In the following example only api scenario and third party tests
+ will be executed.
::
vars:
- tempest_test_regex: |
- (?x) # Ignore comments and whitespaces
- # Line with only a comment.
- (tempest\.(api|scenario|thirdparty)).*$ # Run only api scenario and third party
+ tempest_test_regex: (tempest\.(api|scenario|thirdparty)).*$
.. zuul:rolevar:: tempest_test_blacklist
@@ -48,14 +46,9 @@
It works only when used with some specific tox environments
('all', 'all-plugin'.)
- Multi-line and commented regexs can be achieved by doing this:
-
::
vars:
- tempest_black_regex: |
- (?x) # Ignore comments and whitespaces
- # Line with only a comment.
- (tempest.api.identity).*$
+ tempest_black_regex: (tempest.api.identity).*$
.. zuul:rolevar:: tox_extra_args
:default: ''
diff --git a/setup.cfg b/setup.cfg
index d246c68..04511e1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,7 +6,7 @@
author = OpenStack
author-email = openstack-discuss@lists.openstack.org
home-page = https://docs.openstack.org/tempest/latest/
-requires-python = >=3.6
+python-requires = >=3.6
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
@@ -49,5 +49,3 @@
oslo.config.opts =
tempest.config = tempest.config:list_opts
-[wheel]
-universal = 1
diff --git a/tempest/api/compute/admin/test_aggregates_negative.py b/tempest/api/compute/admin/test_aggregates_negative.py
index a6e0efa..d5adfed 100644
--- a/tempest/api/compute/admin/test_aggregates_negative.py
+++ b/tempest/api/compute/admin/test_aggregates_negative.py
@@ -144,6 +144,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('19dd44e1-c435-4ee1-a402-88c4f90b5950')
def test_aggregate_add_existent_host(self):
+ # Adding already existing host to aggregate should fail.
self.useFixture(fixtures.LockFixture('availability_zone'))
aggregate = self._create_test_aggregate()
@@ -172,6 +173,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('95d6a6fa-8da9-4426-84d0-eec0329f2e4d')
def test_aggregate_remove_nonexistent_host(self):
+ # Removing not existing host from aggregate should fail.
aggregate = self._create_test_aggregate()
self.assertRaises(lib_exc.NotFound, self.client.remove_host,
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 4d27a22..4c531b3 100644
--- a/tempest/api/compute/admin/test_flavors_extra_specs.py
+++ b/tempest/api/compute/admin/test_flavors_extra_specs.py
@@ -61,10 +61,13 @@
@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 = {"key1": "value1", "key2": "value2"}
+ specs = {'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
# SET extra specs to the flavor created in setUp
set_body = self.admin_flavors_client.set_flavor_extra_spec(
self.flavor['id'], **specs)['extra_specs']
@@ -74,30 +77,34 @@
self.flavor['id'])['extra_specs'])
self.assertEqual(get_body, specs)
- # UPDATE the value of the extra specs key1
- update_body = \
- self.admin_flavors_client.update_flavor_extra_spec(
- self.flavor['id'], "key1", key1="value")
- self.assertEqual({"key1": "value"}, update_body)
+ # UPDATE the value of the extra specs 'hw:numa_nodes'
+ update_body = self.admin_flavors_client.update_flavor_extra_spec(
+ self.flavor['id'], "hw:numa_nodes", **{'hw:numa_nodes': '2'})
+ self.assertEqual({'hw:numa_nodes': '2'}, update_body)
- # GET extra specs and verify the value of the key2
+ # GET extra specs and verify the value of the 'hw:cpu_policy'
# is the same as before
get_body = self.admin_flavors_client.list_flavor_extra_specs(
self.flavor['id'])['extra_specs']
- self.assertEqual(get_body, {"key1": "value", "key2": "value2"})
+ self.assertEqual(
+ get_body, {'hw:numa_nodes': '2', 'hw:cpu_policy': 'shared'}
+ )
# UNSET extra specs that were set in this test
- self.admin_flavors_client.unset_flavor_extra_spec(self.flavor['id'],
- "key1")
- self.admin_flavors_client.unset_flavor_extra_spec(self.flavor['id'],
- "key2")
+ self.admin_flavors_client.unset_flavor_extra_spec(
+ self.flavor['id'], 'hw:numa_nodes'
+ )
+ self.admin_flavors_client.unset_flavor_extra_spec(
+ self.flavor['id'], 'hw:cpu_policy'
+ )
get_body = self.admin_flavors_client.list_flavor_extra_specs(
self.flavor['id'])['extra_specs']
self.assertEmpty(get_body)
@decorators.idempotent_id('a99dad88-ae1c-4fba-aeb4-32f898218bd0')
def test_flavor_non_admin_get_all_keys(self):
- specs = {"key1": "value1", "key2": "value2"}
+ """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)
body = (self.flavors_client.list_flavor_extra_specs(
@@ -108,11 +115,15 @@
@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'], key1="value1", key2="value2")['extra_specs']
- self.assertEqual(body['key1'], 'value1')
- self.assertIn('key2', body)
+ self.flavor['id'], **specs
+ )['extra_specs']
+ self.assertEqual(body['hw:numa_nodes'], '1')
+ self.assertIn('hw:cpu_policy', body)
+
body = self.flavors_client.show_flavor_extra_spec(
- self.flavor['id'], 'key1')
- self.assertEqual(body['key1'], 'value1')
- self.assertNotIn('key2', body)
+ self.flavor['id'], 'hw:numa_nodes')
+ self.assertEqual(body['hw:numa_nodes'], '1')
+ self.assertNotIn('hw:cpu_policy', body)
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 5cde39e..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,70 +64,82 @@
@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'],
- key1="value1", key2="value2")
+ **{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'})
@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'], key1="value1", key2="value2")['extra_specs']
- self.assertEqual(body['key1'], 'value1')
+ self.flavor['id'],
+ **{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
+ )['extra_specs']
+ self.assertEqual(body['hw:numa_nodes'], '1')
self.assertRaises(lib_exc.Forbidden,
self.flavors_client.
update_flavor_extra_spec,
self.flavor['id'],
- 'key1',
- key1='value1_new')
+ 'hw:numa_nodes',
+ **{'hw:numa_nodes': '1'})
@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'], key1="value1", key2="value2")
+ self.flavor['id'],
+ **{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'}
+ )
self.assertRaises(lib_exc.Forbidden,
self.flavors_client.unset_flavor_extra_spec,
self.flavor['id'],
- 'key1')
+ 'hw:numa_nodes')
@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'],
- 'nonexistent_key')
+ 'hw:cpu_thread_policy')
@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'],
- "nonexistent_key")
+ 'hw:cpu_thread_policy')
@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'],
- "key2",
- key1="value")
+ 'hw:numa_nodes',
+ **{'hw:cpu_policy': 'shared'})
@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'],
- "key1",
- key1="value",
- key2="value")
+ 'hw:numa_nodes',
+ **{'hw:numa_nodes': '1', 'hw:cpu_policy': 'shared'})
diff --git a/tempest/api/compute/admin/test_hosts.py b/tempest/api/compute/admin/test_hosts.py
index c246685..31fe2b5 100644
--- a/tempest/api/compute/admin/test_hosts.py
+++ b/tempest/api/compute/admin/test_hosts.py
@@ -29,11 +29,13 @@
@decorators.idempotent_id('9bfaf98d-e2cb-44b0-a07e-2558b2821e4f')
def test_list_hosts(self):
+ # Listing hosts.
hosts = self.client.list_hosts()['hosts']
self.assertGreaterEqual(len(hosts), 2, str(hosts))
@decorators.idempotent_id('5dc06f5b-d887-47a2-bb2a-67762ef3c6de')
def test_list_hosts_with_zone(self):
+ # Listing hosts with specified availability zone
self.useFixture(fixtures.LockFixture('availability_zone'))
hosts = self.client.list_hosts()['hosts']
host = hosts[0]
@@ -43,6 +45,7 @@
@decorators.idempotent_id('9af3c171-fbf4-4150-a624-22109733c2a6')
def test_list_hosts_with_a_blank_zone(self):
+ # Listing hosts with blank availability zone.
# If send the request with a blank zone, the request will be successful
# and it will return all the hosts list
hosts = self.client.list_hosts(zone='')['hosts']
@@ -50,6 +53,7 @@
@decorators.idempotent_id('c6ddbadb-c94e-4500-b12f-8ffc43843ff8')
def test_list_hosts_with_nonexistent_zone(self):
+ # Listing hosts with not existing availability zone.
# If send the request with a nonexistent zone, the request will be
# successful and no hosts will be returned
hosts = self.client.list_hosts(zone='xxx')['hosts']
@@ -57,6 +61,7 @@
@decorators.idempotent_id('38adbb12-aee2-4498-8aec-329c72423aa4')
def test_show_host_detail(self):
+ # Showing host details.
hosts = self.client.list_hosts()['hosts']
hosts = [host for host in hosts if host['service'] == 'compute']
diff --git a/tempest/api/compute/admin/test_hosts_negative.py b/tempest/api/compute/admin/test_hosts_negative.py
index 8a91ae2..e8733c8 100644
--- a/tempest/api/compute/admin/test_hosts_negative.py
+++ b/tempest/api/compute/admin/test_hosts_negative.py
@@ -39,18 +39,21 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('dd032027-0210-4d9c-860e-69b1b8deed5f')
def test_list_hosts_with_non_admin_user(self):
+ # Non admin user is not allowed to list hosts.
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.list_hosts)
@decorators.attr(type=['negative'])
@decorators.idempotent_id('e75b0a1a-041f-47a1-8b4a-b72a6ff36d3f')
def test_show_host_detail_with_nonexistent_hostname(self):
+ # Showing host detail with not existing hostname should fail.
self.assertRaises(lib_exc.NotFound,
self.client.show_host, 'nonexistent_hostname')
@decorators.attr(type=['negative'])
@decorators.idempotent_id('19ebe09c-bfd4-4b7c-81a2-e2e0710f59cc')
def test_show_host_detail_with_non_admin_user(self):
+ # Non admin user is not allowed to show host details.
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.show_host,
self.hostname)
@@ -58,6 +61,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('e40c72b1-0239-4ed6-ba21-81a184df1f7c')
def test_update_host_with_non_admin_user(self):
+ # Non admin user is not allowed to update host.
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.update_host,
self.hostname,
@@ -67,7 +71,8 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('fbe2bf3e-3246-4a95-a59f-94e4e298ec77')
def test_update_host_with_invalid_status(self):
- # 'status' can only be 'enable' or 'disable'
+ # Updating host to invalid status should fail,
+ # 'status' can only be 'enable' or 'disable'.
self.assertRaises(lib_exc.BadRequest,
self.client.update_host,
self.hostname,
@@ -77,7 +82,8 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('ab1e230e-5e22-41a9-8699-82b9947915d4')
def test_update_host_with_invalid_maintenance_mode(self):
- # 'maintenance_mode' can only be 'enable' or 'disable'
+ # Updating host to invalid maintenance mode should fail,
+ # 'maintenance_mode' can only be 'enable' or 'disable'.
self.assertRaises(lib_exc.BadRequest,
self.client.update_host,
self.hostname,
@@ -87,7 +93,8 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('0cd85f75-6992-4a4a-b1bd-d11e37fd0eee')
def test_update_host_without_param(self):
- # 'status' or 'maintenance_mode' needed for host update
+ # Updating host without param should fail,
+ # 'status' or 'maintenance_mode' is needed for host update.
self.assertRaises(lib_exc.BadRequest,
self.client.update_host,
self.hostname)
@@ -95,6 +102,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('23c92146-2100-4d68-b2d6-c7ade970c9c1')
def test_update_nonexistent_host(self):
+ # Updating not existing host should fail.
self.assertRaises(lib_exc.NotFound,
self.client.update_host,
'nonexistent_hostname',
@@ -104,6 +112,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('0d981ac3-4320-4898-b674-82b61fbb60e4')
def test_startup_nonexistent_host(self):
+ # Starting up not existing host should fail.
self.assertRaises(lib_exc.NotFound,
self.client.startup_host,
'nonexistent_hostname')
@@ -111,6 +120,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('9f4ebb7e-b2ae-4e5b-a38f-0fd1bb0ddfca')
def test_startup_host_with_non_admin_user(self):
+ # Non admin user is not allowed to startup host.
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.startup_host,
self.hostname)
@@ -118,6 +128,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('9e637444-29cf-4244-88c8-831ae82c31b6')
def test_shutdown_nonexistent_host(self):
+ # Shutting down not existing host should fail.
self.assertRaises(lib_exc.NotFound,
self.client.shutdown_host,
'nonexistent_hostname')
@@ -125,6 +136,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('a803529c-7e3f-4d3c-a7d6-8e1c203d27f6')
def test_shutdown_host_with_non_admin_user(self):
+ # Non admin user is not allowed to shutdown host.
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.shutdown_host,
self.hostname)
@@ -132,6 +144,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('f86bfd7b-0b13-4849-ae29-0322e83ee58b')
def test_reboot_nonexistent_host(self):
+ # Rebooting not existing host should fail.
self.assertRaises(lib_exc.NotFound,
self.client.reboot_host,
'nonexistent_hostname')
@@ -139,6 +152,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('02d79bb9-eb57-4612-abf6-2cb38897d2f8')
def test_reboot_host_with_non_admin_user(self):
+ # Non admin user is not allowed to reboot host.
self.assertRaises(lib_exc.Forbidden,
self.non_admin_client.reboot_host,
self.hostname)
diff --git a/tempest/api/compute/admin/test_hypervisor.py b/tempest/api/compute/admin/test_hypervisor.py
index 9822c26..e45aac5 100644
--- a/tempest/api/compute/admin/test_hypervisor.py
+++ b/tempest/api/compute/admin/test_hypervisor.py
@@ -134,6 +134,7 @@
@decorators.idempotent_id('d7e1805b-3b14-4a3b-b6fd-50ec6d9f361f')
def test_search_hypervisor(self):
+ # Searching for hypervisors by its name.
hypers = self._list_hypervisors()
self.assertNotEmpty(hypers, "No hypervisors found.")
hypers = self.client.search_hypervisor(
diff --git a/tempest/api/compute/admin/test_hypervisor_negative.py b/tempest/api/compute/admin/test_hypervisor_negative.py
index 0056376..723b93c 100644
--- a/tempest/api/compute/admin/test_hypervisor_negative.py
+++ b/tempest/api/compute/admin/test_hypervisor_negative.py
@@ -40,8 +40,8 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('c136086a-0f67-4b2b-bc61-8482bd68989f')
def test_show_nonexistent_hypervisor(self):
+ # Showing not existing hypervisor should fail.
nonexistent_hypervisor_id = data_utils.rand_uuid()
-
self.assertRaises(
lib_exc.NotFound,
self.client.show_hypervisor,
@@ -50,6 +50,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('51e663d0-6b89-4817-a465-20aca0667d03')
def test_show_hypervisor_with_non_admin_user(self):
+ # Non admin user is not allowed to show hypervisor.
hypers = self._list_hypervisors()
self.assertNotEmpty(hypers)
@@ -61,6 +62,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('e2b061bb-13f9-40d8-9d6e-d5bf17595849')
def test_get_hypervisor_stats_with_non_admin_user(self):
+ # Non admin user is not allowed to get hypervisor stats.
self.assertRaises(
lib_exc.Forbidden,
self.non_adm_client.show_hypervisor_statistics)
@@ -68,6 +70,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('f60aa680-9a3a-4c7d-90e1-fae3a4891303')
def test_get_nonexistent_hypervisor_uptime(self):
+ # Getting uptime of not existing hypervisor should fail.
nonexistent_hypervisor_id = data_utils.rand_uuid()
self.assertRaises(
@@ -78,6 +81,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('6c3461f9-c04c-4e2a-bebb-71dc9cb47df2')
def test_get_hypervisor_uptime_with_non_admin_user(self):
+ # Non admin user is not allowed to get hypervisor uptime.
hypers = self._list_hypervisors()
self.assertNotEmpty(hypers)
@@ -97,7 +101,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('dc02db05-e801-4c5f-bc8e-d915290ab345')
def test_get_hypervisor_list_details_with_non_admin_user(self):
- # List of hypervisor details and available services with non admin user
+ # Non admin user is not allowed to list hypervisor details.
self.assertRaises(
lib_exc.Forbidden,
self.non_adm_client.list_hypervisors, detail=True)
@@ -109,6 +113,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('2a0a3938-832e-4859-95bf-1c57c236b924')
def test_show_servers_with_non_admin_user(self):
+ # Non admin user is not allowed to show servers on hypervisor.
hypers = self._list_hypervisors()
self.assertNotEmpty(hypers)
@@ -120,6 +125,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('02463d69-0ace-4d33-a4a8-93d7883a2bba')
def test_show_servers_with_nonexistent_hypervisor(self):
+ # Showing servers on not existing hypervisor should fail.
nonexistent_hypervisor_id = data_utils.rand_uuid()
self.assertRaises(
@@ -130,6 +136,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('5b6a6c79-5dc1-4fa5-9c58-9c8085948e74')
def test_search_hypervisor_with_non_admin_user(self):
+ # Non admin user is not allowed to search hypervisor.
hypers = self._list_hypervisors()
self.assertNotEmpty(hypers)
@@ -141,6 +148,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('19a45cc1-1000-4055-b6d2-28e8b2ec4faa')
def test_search_nonexistent_hypervisor(self):
+ # Searching not existing hypervisor should fail.
self.assertRaises(
lib_exc.NotFound,
self.client.search_hypervisor,
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.py b/tempest/api/compute/admin/test_services.py
index 73e191b..bf846e5 100644
--- a/tempest/api/compute/admin/test_services.py
+++ b/tempest/api/compute/admin/test_services.py
@@ -28,11 +28,13 @@
@decorators.idempotent_id('5be41ef4-53d1-41cc-8839-5c2a48a1b283')
def test_list_services(self):
+ # Listing nova services
services = self.client.list_services()['services']
self.assertNotEmpty(services)
@decorators.idempotent_id('f345b1ec-bc6e-4c38-a527-3ca2bc00bef5')
def test_get_service_by_service_binary_name(self):
+ # Listing nova services by binary name.
binary_name = 'nova-compute'
services = self.client.list_services(binary=binary_name)['services']
self.assertNotEmpty(services)
@@ -41,6 +43,7 @@
@decorators.idempotent_id('affb42d5-5b4b-43c8-8b0b-6dca054abcca')
def test_get_service_by_host_name(self):
+ # Listing nova services by host name.
services = self.client.list_services()['services']
host_name = services[0]['host']
services_on_host = [service for service in services if
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_volume_swap.py b/tempest/api/compute/admin/test_volume_swap.py
index 371b506..edcb1a7 100644
--- a/tempest/api/compute/admin/test_volume_swap.py
+++ b/tempest/api/compute/admin/test_volume_swap.py
@@ -23,6 +23,7 @@
class TestVolumeSwapBase(base.BaseV2ComputeAdminTest):
+ create_default_network = True
@classmethod
def skip_checks(cls):
diff --git a/tempest/api/compute/admin/test_volumes_negative.py b/tempest/api/compute/admin/test_volumes_negative.py
index 4a7f36f..7b0f48b 100644
--- a/tempest/api/compute/admin/test_volumes_negative.py
+++ b/tempest/api/compute/admin/test_volumes_negative.py
@@ -13,6 +13,7 @@
# under the License.
from tempest.api.compute import base
+from tempest.common import utils
from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
@@ -22,6 +23,7 @@
class VolumesAdminNegativeTest(base.BaseV2ComputeAdminTest):
+ create_default_network = True
@classmethod
def skip_checks(cls):
@@ -57,3 +59,66 @@
self.admin_servers_client.update_attached_volume,
self.server['id'], volume['id'],
volumeId=nonexistent_volume)
+
+
+class UpdateMultiattachVolumeNegativeTest(base.BaseV2ComputeAdminTest):
+
+ min_microversion = '2.60'
+ volume_min_microversion = '3.27'
+
+ @classmethod
+ def skip_checks(cls):
+ super(UpdateMultiattachVolumeNegativeTest, cls).skip_checks()
+ if not CONF.compute_feature_enabled.volume_multiattach:
+ raise cls.skipException('Volume multi-attach is not available.')
+
+ @decorators.attr(type=['negative'])
+ @decorators.idempotent_id('7576d497-b7c6-44bd-9cc5-c5b4e50fec71')
+ @utils.services('volume')
+ def test_multiattach_rw_volume_update_failure(self):
+
+ # Create two multiattach capable volumes.
+ vol1 = self.create_volume(multiattach=True)
+ vol2 = self.create_volume(multiattach=True)
+
+ # Create two instances.
+ server1 = self.create_test_server(wait_until='ACTIVE')
+ server2 = self.create_test_server(wait_until='ACTIVE')
+
+ # Attach vol1 to both of these instances.
+ vol1_attachment1 = self.attach_volume(server1, vol1)
+ vol1_attachment2 = self.attach_volume(server2, vol1)
+
+ # Assert that we now have two attachments.
+ vol1 = self.volumes_client.show_volume(vol1['id'])['volume']
+ self.assertEqual(2, len(vol1['attachments']))
+
+ # By default both of these attachments should have an attach_mode of
+ # read-write, assert that here to ensure the following calls to update
+ # the volume will be rejected.
+ for volume_attachment in vol1['attachments']:
+ attachment_id = volume_attachment['attachment_id']
+ attachment = self.attachments_client.show_attachment(
+ attachment_id)['attachment']
+ self.assertEqual('rw', attachment['attach_mode'])
+
+ # Assert that a BadRequest is raised when we attempt to update volume1
+ # to volume2 on server1 or server2.
+ self.assertRaises(lib_exc.BadRequest,
+ self.admin_servers_client.update_attached_volume,
+ server1['id'], vol1['id'], volumeId=vol2['id'])
+ self.assertRaises(lib_exc.BadRequest,
+ self.admin_servers_client.update_attached_volume,
+ server2['id'], vol1['id'], volumeId=vol2['id'])
+
+ # Fetch the volume 1 to check the current attachments.
+ vol1 = self.volumes_client.show_volume(vol1['id'])['volume']
+ vol1_attachment_ids = [a['id'] for a in vol1['attachments']]
+
+ # Assert that volume 1 is still attached to both server 1 and 2.
+ self.assertIn(vol1_attachment1['id'], vol1_attachment_ids)
+ self.assertIn(vol1_attachment2['id'], vol1_attachment_ids)
+
+ # Assert that volume 2 has no attachments.
+ vol2 = self.volumes_client.show_volume(vol2['id'])['volume']
+ self.assertEqual([], vol2['attachments'])
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 8d0962d..eab2a8d 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -108,6 +108,7 @@
cls.versions_client = cls.os_primary.compute_versions_client
if CONF.service_available.cinder:
cls.volumes_client = cls.os_primary.volumes_client_latest
+ cls.attachments_client = cls.os_primary.attachments_client_latest
if CONF.service_available.glance:
if CONF.image_feature_enabled.api_v1:
cls.images_client = cls.os_primary.image_client
@@ -226,7 +227,7 @@
@classmethod
def create_test_server(cls, validatable=False, volume_backed=False,
- validation_resources=None, **kwargs):
+ validation_resources=None, clients=None, **kwargs):
"""Wrapper utility that returns a test server.
This wrapper utility calls the common create test server and
@@ -238,6 +239,7 @@
:param volume_backed: Whether the instance is volume backed or not.
:param validation_resources: Dictionary of validation resources as
returned by `get_class_validation_resources`.
+ :param clients: Client manager, defaults to os_primary.
:param kwargs: Extra arguments are passed down to the
`compute.create_test_server` call.
"""
@@ -254,8 +256,11 @@
not tenant_network):
kwargs['networks'] = 'none'
+ if clients is None:
+ clients = cls.os_primary
+
body, servers = compute.create_test_server(
- cls.os_primary,
+ clients,
validatable,
validation_resources=validation_resources,
tenant_network=tenant_network,
@@ -266,11 +271,11 @@
# and then wait for all
for server in servers:
cls.addClassResourceCleanup(waiters.wait_for_server_termination,
- cls.servers_client, server['id'])
+ clients.servers_client, server['id'])
for server in servers:
cls.addClassResourceCleanup(
test_utils.call_and_ignore_notfound_exc,
- cls.servers_client.delete_server, server['id'])
+ clients.servers_client.delete_server, server['id'])
return body
diff --git a/tempest/api/compute/images/test_images.py b/tempest/api/compute/images/test_images.py
index eef2781..ef33685 100644
--- a/tempest/api/compute/images/test_images.py
+++ b/tempest/api/compute/images/test_images.py
@@ -25,6 +25,7 @@
class ImagesTestJSON(base.BaseV2ComputeTest):
+ create_default_network = True
@classmethod
def skip_checks(cls):
diff --git a/tempest/api/compute/images/test_images_oneserver_negative.py b/tempest/api/compute/images/test_images_oneserver_negative.py
index 512c9d2..37f9be3 100644
--- a/tempest/api/compute/images/test_images_oneserver_negative.py
+++ b/tempest/api/compute/images/test_images_oneserver_negative.py
@@ -30,6 +30,7 @@
class ImagesOneServerNegativeTestJSON(base.BaseV2ComputeTest):
+ create_default_network = True
def tearDown(self):
"""Terminate test instances created after a test is executed."""
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index df8da07..c1af6c7 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -86,12 +86,16 @@
# apparently not enough? Add cleanup here.
self.addCleanup(self.delete_server, server['id'])
self._wait_for_validation(server, validation_resources)
+ try:
+ fip = set([validation_resources['floating_ip']['ip']])
+ except KeyError:
+ fip = ()
ifs = (self.interfaces_client.list_interfaces(server['id'])
['interfaceAttachments'])
body = waiters.wait_for_interface_status(
self.interfaces_client, server['id'], ifs[0]['port_id'], 'ACTIVE')
ifs[0]['port_state'] = body['port_state']
- return server, ifs
+ return server, ifs, fip
class AttachInterfacesTestJSON(AttachInterfacesTestBase):
@@ -226,7 +230,7 @@
@decorators.idempotent_id('73fe8f02-590d-4bf1-b184-e9ca81065051')
@utils.services('network')
def test_create_list_show_delete_interfaces_by_network_port(self):
- server, ifs = self._create_server_get_interfaces()
+ server, ifs, _ = self._create_server_get_interfaces()
interface_count = len(ifs)
self.assertGreater(interface_count, 0)
@@ -268,7 +272,7 @@
raise self.skipException("Only owner network supports "
"creating interface by fixed ip.")
- server, ifs = self._create_server_get_interfaces()
+ server, ifs, _ = self._create_server_get_interfaces()
interface_count = len(ifs)
self.assertGreater(interface_count, 0)
@@ -354,9 +358,8 @@
not CONF.network.shared_physical_network):
raise self.skipException("Only owner network supports "
"creating interface by fixed ip.")
-
# Add and Remove the fixed IP to server.
- server, ifs = self._create_server_get_interfaces()
+ server, ifs, fip = self._create_server_get_interfaces()
original_interface_count = len(ifs) # This is the number of ports.
self.assertGreater(original_interface_count, 0)
# Get the starting list of IPs on the server.
@@ -369,6 +372,9 @@
self.assertEqual(1, len(addresses), addresses) # number of networks
# Keep track of the original addresses so we can know which IP is new.
original_ips = [addr['addr'] for addr in list(addresses.values())[0]]
+ # Make sure the floating IP possibly assigned during
+ # server creation is always present in the set of original ips.
+ original_ips = set(original_ips).union(fip)
original_ip_count = len(original_ips)
self.assertGreater(original_ip_count, 0, addresses) # at least 1
network_id = ifs[0]['net_id']
@@ -376,40 +382,22 @@
# fixed IP on the same network (and same port since we only have one
# port).
self.servers_client.add_fixed_ip(server['id'], networkId=network_id)
- # Wait for the ips count to increase by one.
- def _get_server_floating_ips():
- _floating_ips = []
- _server = self.os_primary.servers_client.show_server(
- server['id'])['server']
- for _ip_set in _server['addresses']:
- for _ip in _server['addresses'][_ip_set]:
- if _ip['OS-EXT-IPS:type'] == 'floating':
- _floating_ips.append(_ip['addr'])
- return _floating_ips
-
- def _wait_for_ip_increase():
+ def _wait_for_ip_change(expected_count):
_addresses = self.os_primary.servers_client.list_addresses(
server['id'])['addresses']
- _ips = [addr['addr'] for addr in list(_addresses.values())[0]]
- LOG.debug("Wait for IP increase. All IPs still associated to "
+ _ips = set([addr['addr'] for addr in list(_addresses.values())[0]])
+ # Make sure possible floating ip is always present in the set.
+ _ips = _ips.union(fip)
+ LOG.debug("Wait for change of IPs. All IPs still associated to "
"the server %(id)s: %(ips)s",
{'id': server['id'], 'ips': _ips})
- if len(_ips) == original_ip_count + 1:
- return True
- elif len(_ips) == original_ip_count:
- return False
- # If not, lets remove any floating IP from the list and check again
- _fips = _get_server_floating_ips()
- _ips = [_ip for _ip in _ips if _ip not in _fips]
- LOG.debug("Wait for IP increase. Fixed IPs still associated to "
- "the server %(id)s: %(ips)s",
- {'id': server['id'], 'ips': _ips})
- return len(_ips) == original_ip_count + 1
+ return len(_ips) == expected_count
+ # Wait for the ips count to increase by one.
if not test_utils.call_until_true(
- _wait_for_ip_increase, CONF.compute.build_timeout,
- CONF.compute.build_interval):
+ _wait_for_ip_change, CONF.compute.build_timeout,
+ CONF.compute.build_interval, original_ip_count + 1):
raise lib_exc.TimeoutException(
'Timed out while waiting for IP count to increase.')
@@ -428,26 +416,8 @@
break
self.servers_client.remove_fixed_ip(server['id'], address=fixed_ip)
# Wait for the interface count to decrease by one.
-
- def _wait_for_ip_decrease():
- _addresses = self.os_primary.servers_client.list_addresses(
- server['id'])['addresses']
- _ips = [addr['addr'] for addr in list(_addresses.values())[0]]
- LOG.debug("Wait for IP decrease. All IPs still associated to "
- "the server %(id)s: %(ips)s",
- {'id': server['id'], 'ips': _ips})
- if len(_ips) == original_ip_count:
- return True
- # If not, lets remove any floating IP from the list and check again
- _fips = _get_server_floating_ips()
- _ips = [_ip for _ip in _ips if _ip not in _fips]
- LOG.debug("Wait for IP decrease. Fixed IPs still associated to "
- "the server %(id)s: %(ips)s",
- {'id': server['id'], 'ips': _ips})
- return len(_ips) == original_ip_count
-
if not test_utils.call_until_true(
- _wait_for_ip_decrease, CONF.compute.build_timeout,
- CONF.compute.build_interval):
+ _wait_for_ip_change, CONF.compute.build_timeout,
+ CONF.compute.build_interval, original_ip_count):
raise lib_exc.TimeoutException(
'Timed out while waiting for IP count to decrease.')
diff --git a/tempest/api/compute/servers/test_multiple_create.py b/tempest/api/compute/servers/test_multiple_create.py
index e176251..dcadace 100644
--- a/tempest/api/compute/servers/test_multiple_create.py
+++ b/tempest/api/compute/servers/test_multiple_create.py
@@ -23,6 +23,7 @@
@decorators.idempotent_id('61e03386-89c3-449c-9bb1-a06f423fd9d1')
def test_multiple_create(self):
+ # Creating server with min_count=2, 2 servers will be created.
tenant_network = self.get_tenant_network()
body, servers = compute.create_test_server(
self.os_primary,
@@ -39,6 +40,8 @@
@decorators.idempotent_id('864777fb-2f1e-44e3-b5b9-3eb6fa84f2f7')
def test_multiple_create_with_reservation_return(self):
+ # Creating multiple servers with return_reservation_id=True,
+ # reservation_id will be returned.
body = self.create_test_server(wait_until='ACTIVE',
min_count=1,
max_count=2,
diff --git a/tempest/api/compute/servers/test_multiple_create_negative.py b/tempest/api/compute/servers/test_multiple_create_negative.py
index 422510f..6bdf83b 100644
--- a/tempest/api/compute/servers/test_multiple_create_negative.py
+++ b/tempest/api/compute/servers/test_multiple_create_negative.py
@@ -23,6 +23,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('daf29d8d-e928-4a01-9a8c-b129603f3fc0')
def test_min_count_less_than_one(self):
+ # Creating server with min_count=0 should fail.
invalid_min_count = 0
self.assertRaises(lib_exc.BadRequest, self.create_test_server,
min_count=invalid_min_count)
@@ -30,6 +31,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('999aa722-d624-4423-b813-0d1ac9884d7a')
def test_min_count_non_integer(self):
+ # Creating server with non-integer min_count should fail.
invalid_min_count = 2.5
self.assertRaises(lib_exc.BadRequest, self.create_test_server,
min_count=invalid_min_count)
@@ -37,6 +39,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('a6f9c2ab-e060-4b82-b23c-4532cb9390ff')
def test_max_count_less_than_one(self):
+ # Creating server with max_count < 1 shoudld fail.
invalid_max_count = 0
self.assertRaises(lib_exc.BadRequest, self.create_test_server,
max_count=invalid_max_count)
@@ -44,6 +47,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('9c5698d1-d7af-4c80-b971-9d403135eea2')
def test_max_count_non_integer(self):
+ # Creating server with non-integer max_count should fail.
invalid_max_count = 2.5
self.assertRaises(lib_exc.BadRequest, self.create_test_server,
max_count=invalid_max_count)
@@ -51,6 +55,7 @@
@decorators.attr(type=['negative'])
@decorators.idempotent_id('476da616-f1ef-4271-a9b1-b9fc87727cdf')
def test_max_count_less_than_min_count(self):
+ # Creating server with max_count less than min_count should fail.
min_count = 3
max_count = 2
self.assertRaises(lib_exc.BadRequest, self.create_test_server,
diff --git a/tempest/api/compute/servers/test_server_metadata_negative.py b/tempest/api/compute/servers/test_server_metadata_negative.py
index 482ba09..5688af1 100644
--- a/tempest/api/compute/servers/test_server_metadata_negative.py
+++ b/tempest/api/compute/servers/test_server_metadata_negative.py
@@ -20,6 +20,7 @@
class ServerMetadataNegativeTestJSON(base.BaseV2ComputeTest):
+ create_default_network = True
@classmethod
def setup_clients(cls):
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_groups.py b/tempest/api/identity/admin/v3/test_groups.py
index df0d79d..2dd1fe2 100644
--- a/tempest/api/identity/admin/v3/test_groups.py
+++ b/tempest/api/identity/admin/v3/test_groups.py
@@ -114,6 +114,13 @@
self.groups_client.add_group_user(group['id'], user['id'])
# list groups which user belongs to
user_groups = self.users_client.list_user_groups(user['id'])['groups']
+ # The `membership_expires_at` attribute is present when listing user
+ # group memberships, and is not an attribute of the groups themselves.
+ # Therefore we remove it from the comparison.
+ for g in user_groups:
+ if 'membership_expires_at' in g:
+ self.assertIsNone(g['membership_expires_at'])
+ del(g['membership_expires_at'])
self.assertEqual(sorted(groups, key=lambda k: k['name']),
sorted(user_groups, key=lambda k: k['name']))
self.assertEqual(2, len(user_groups))
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/network/test_tags.py b/tempest/api/network/test_tags.py
index 85f6896..2b9719a 100644
--- a/tempest/api/network/test_tags.py
+++ b/tempest/api/network/test_tags.py
@@ -103,9 +103,10 @@
List tags.
Remove a tag.
- v2.0 of the Neutron API is assumed. The tag-ext extension allows users to
- set tags on the following resources: subnets, ports, routers and
- subnetpools.
+ v2.0 of the Neutron API is assumed. The tag-ext or standard-attr-tag
+ extension allows users to set tags on the following resources: subnets,
+ ports, routers and subnetpools.
+ from stein release the tag-ext has been renamed to standard-attr-tag
"""
# NOTE(felipemonteiro): The supported resource names are plural. Use
@@ -115,8 +116,12 @@
@classmethod
def skip_checks(cls):
super(TagsExtTest, cls).skip_checks()
- if not utils.is_extension_enabled('tag-ext', 'network'):
- msg = "tag-ext extension not enabled."
+ # Added condition to support backward compatiblity since
+ # tag-ext has been renamed to standard-attr-tag
+ if not (utils.is_extension_enabled('tag-ext', 'network') or
+ utils.is_extension_enabled('standard-attr-tag', 'network')):
+ msg = ("neither tag-ext nor standard-attr-tag extensions "
+ "are enabled.")
raise cls.skipException(msg)
@classmethod
diff --git a/tempest/api/volume/admin/test_group_snapshots.py b/tempest/api/volume/admin/test_group_snapshots.py
index f695f51..c57766e 100644
--- a/tempest/api/volume/admin/test_group_snapshots.py
+++ b/tempest/api/volume/admin/test_group_snapshots.py
@@ -113,7 +113,8 @@
self._delete_group_snapshot(group_snapshot)
group_snapshots = self.group_snapshots_client.list_group_snapshots()[
'group_snapshots']
- self.assertEmpty(group_snapshots)
+ self.assertNotIn((group_snapshot['name'], group_snapshot['id']),
+ [(m['name'], m['id']) for m in group_snapshots])
@decorators.idempotent_id('eff52c70-efc7-45ed-b47a-4ad675d09b81')
def test_create_group_from_group_snapshot(self):
diff --git a/tempest/api/volume/admin/test_volumes_actions.py b/tempest/api/volume/admin/test_volumes_actions.py
index 3e0deef..5bac3d8 100644
--- a/tempest/api/volume/admin/test_volumes_actions.py
+++ b/tempest/api/volume/admin/test_volumes_actions.py
@@ -23,6 +23,7 @@
class VolumesActionsTest(base.BaseVolumeAdminTest):
+ create_default_network = True
def _create_reset_and_force_delete_temp_volume(self, status=None):
# Create volume, reset volume status, and force delete temp volume
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 1bfd075..bcbcf43 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -30,6 +30,9 @@
tempest.test.BaseTestCase):
"""Base test case class for all Cinder API tests."""
+ # Set this to True in subclasses to create a default network. See
+ # https://bugs.launchpad.net/tempest/+bug/1844568
+ create_default_network = False
_api_version = 2
# if api_v2 is not enabled while api_v3 is enabled, the volume v2 classes
# should be transferred to volume v3 classes.
@@ -63,7 +66,9 @@
@classmethod
def setup_credentials(cls):
- cls.set_network_resources()
+ cls.set_network_resources(
+ network=cls.create_default_network,
+ subnet=cls.create_default_network)
super(BaseVolumeTest, cls).setup_credentials()
@classmethod
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_absolute_limits.py b/tempest/api/volume/test_volume_absolute_limits.py
index 00a3375..4d64a95 100644
--- a/tempest/api/volume/test_volume_absolute_limits.py
+++ b/tempest/api/volume/test_volume_absolute_limits.py
@@ -23,7 +23,7 @@
# NOTE(zhufl): This inherits from BaseVolumeAdminTest because
# it requires force_tenant_isolation=True, which need admin
# credentials to create non-admin users for the tests.
-class AbsoluteLimitsTests(base.BaseVolumeAdminTest): # noqa
+class AbsoluteLimitsTests(base.BaseVolumeAdminTest): # noqa: T115
# avoid existing volumes of pre-defined tenant
force_tenant_isolation = True
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_actions.py b/tempest/api/volume/test_volumes_actions.py
index be5638e..9edffc6 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -25,6 +25,7 @@
class VolumesActionsTest(base.BaseVolumeTest):
+ create_default_network = True
@classmethod
def resource_setup(cls):
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 c3f44e2..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'])
@@ -60,6 +63,7 @@
class VolumesExtendAttachedTest(base.BaseVolumeTest):
"""Tests extending the size of an attached volume."""
+ create_default_network = True
# We need admin credentials for getting instance action event details. By
# default a non-admin can list and show instance actions if they own the
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/api/volume/test_volumes_snapshots.py b/tempest/api/volume/test_volumes_snapshots.py
index 72e7290..bf221e8 100644
--- a/tempest/api/volume/test_volumes_snapshots.py
+++ b/tempest/api/volume/test_volumes_snapshots.py
@@ -25,6 +25,7 @@
class VolumesSnapshotTestJSON(base.BaseVolumeTest):
+ create_default_network = True
@classmethod
def skip_checks(cls):
diff --git a/tempest/api/volume/test_volumes_snapshots_list.py b/tempest/api/volume/test_volumes_snapshots_list.py
index 8a416ea..f4f039c 100644
--- a/tempest/api/volume/test_volumes_snapshots_list.py
+++ b/tempest/api/volume/test_volumes_snapshots_list.py
@@ -109,7 +109,7 @@
snap_list = self.snapshots_client.list_snapshots(
sort_key=sort_key, sort_dir=sort_dir)['snapshots']
self.assertNotEmpty(snap_list)
- if sort_key is 'display_name':
+ if sort_key == 'display_name':
sort_key = 'name'
# Note: On Cinder API, 'display_name' works as a sort key
# on a request, a volume name appears as 'name' on the response.
diff --git a/tempest/clients.py b/tempest/clients.py
index 6aed92e..1db93a0 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -263,6 +263,8 @@
self.volume_v3.MessagesClient())
self.volume_versions_client_latest = (
self.volume_v3.VersionsClient())
+ self.attachments_client_latest = (
+ self.volume_v3.AttachmentsClient())
# TODO(gmann): Below alias for service clients have been
# deprecated and will be removed in future. Start using the alias
diff --git a/tempest/cmd/cleanup_service.py b/tempest/cmd/cleanup_service.py
index 5b3b72a..469b214 100644
--- a/tempest/cmd/cleanup_service.py
+++ b/tempest/cmd/cleanup_service.py
@@ -13,6 +13,7 @@
# under the License.
from oslo_log import log as logging
+from six.moves.urllib import parse as urllib
from tempest import clients
from tempest.common import credentials_factory as credentials
@@ -833,7 +834,15 @@
def list(self):
client = self.client
- images = client.list_images(params={"all_tenants": True})['images']
+ response = client.list_images()
+ images = []
+ images.extend(response['images'])
+ while 'next' in response:
+ parsed = urllib.urlparse(response['next'])
+ marker = urllib.parse_qs(parsed.query)['marker'][0]
+ response = client.list_images(params={"marker": marker})
+ images.extend(response['images'])
+
if not self.is_save_state:
images = [image for image in images if image['id']
not in self.saved_state_json['images'].keys()]
@@ -1039,6 +1048,8 @@
project_associated_services.append(NovaQuotaService)
if IS_CINDER:
project_associated_services.append(VolumeQuotaService)
+ if IS_NEUTRON:
+ project_associated_services.append(NetworkQuotaService)
return project_associated_services
@@ -1067,7 +1078,6 @@
resource_cleanup_services.append(NetworkService)
resource_cleanup_services.append(NetworkSecGroupService)
resource_cleanup_services.append(NetworkSubnetPoolsService)
- resource_cleanup_services.append(NetworkQuotaService)
if IS_CINDER:
resource_cleanup_services.append(SnapshotService)
resource_cleanup_services.append(VolumeService)
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 b547cc6..14790d6 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -124,6 +124,12 @@
raise lib_exc.DeleteErrorException(
"Server %s failed to delete and is in ERROR status" %
server_id)
+ if server_status == 'SOFT_DELETED':
+ # Soft-deleted instances need to be forcibly deleted to
+ # prevent some test cases from failing.
+ LOG.debug("Automatically force-deleting soft-deleted server %s",
+ server_id)
+ client.force_delete_server(server_id)
if int(time.time()) - start_time >= client.build_timeout:
raise lib_exc.TimeoutException
@@ -224,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 5a2d722..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 '
@@ -699,6 +703,11 @@
cfg.StrOpt('floating_network_name',
help="Default floating network name. Used to allocate floating "
"IPs when neutron is enabled."),
+ cfg.StrOpt('subnet_id',
+ default="",
+ help="Subnet id of subnet which is used for allocation of "
+ "floating IPs. Specify when two or more subnets are "
+ "present in network."),
cfg.StrOpt('public_router_id',
default="",
help="Id of the public router that provides external "
diff --git a/tempest/hacking/checks.py b/tempest/hacking/checks.py
index 2c40cb1..6a97a00 100644
--- a/tempest/hacking/checks.py
+++ b/tempest/hacking/checks.py
@@ -15,6 +15,7 @@
import os
import re
+from hacking import core
import pycodestyle
@@ -25,7 +26,6 @@
TEST_DEFINITION = re.compile(r'^\s*def test.*')
SETUP_TEARDOWN_CLASS_DEFINITION = re.compile(r'^\s+def (setUp|tearDown)Class')
SCENARIO_DECORATOR = re.compile(r'\s*@.*services\((.*)\)')
-VI_HEADER_RE = re.compile(r"^#\s+vim?:.+")
RAND_NAME_HYPHEN_RE = re.compile(r".*rand_name\(.+[\-\_][\"\']\)")
mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])")
TESTTOOLS_SKIP_DECORATOR = re.compile(r'\s*@testtools\.skip\((.*)\)')
@@ -39,6 +39,7 @@
_HAVE_NEGATIVE_DECORATOR = False
+@core.flake8ext
def import_no_clients_in_api_and_scenario_tests(physical_line, filename):
"""Check for client imports from tempest/api & tempest/scenario tests
@@ -53,6 +54,7 @@
" in tempest/api/* or tempest/scenario/* tests"))
+@core.flake8ext
def scenario_tests_need_service_tags(physical_line, filename,
previous_logical):
"""Check that scenario tests have service tags
@@ -67,6 +69,7 @@
"T104: Scenario tests require a service decorator")
+@core.flake8ext
def no_setup_teardown_class_for_tests(physical_line, filename):
if pycodestyle.noqa(physical_line):
@@ -80,20 +83,7 @@
"T105: (setUp|tearDown)Class can not be used in tests")
-def no_vi_headers(physical_line, line_number, lines):
- """Check for vi editor configuration in source files.
-
- By default vi modelines can only appear in the first or
- last 5 lines of a source file.
-
- T106
- """
- # NOTE(gilliard): line_number is 1-indexed
- if line_number <= 5 or line_number > len(lines) - 5:
- if VI_HEADER_RE.match(physical_line):
- return 0, "T106: Don't put vi configuration in source files"
-
-
+@core.flake8ext
def service_tags_not_in_module_path(physical_line, filename):
"""Check that a service tag isn't in the module path
@@ -117,6 +107,7 @@
"T107: service tag should not be in path")
+@core.flake8ext
def no_hyphen_at_end_of_rand_name(logical_line, filename):
"""Check no hyphen at the end of rand_name() argument
@@ -127,6 +118,7 @@
return 0, msg
+@core.flake8ext
def no_mutable_default_args(logical_line):
"""Check that mutable object isn't used as default argument
@@ -137,6 +129,7 @@
yield (0, msg)
+@core.flake8ext
def no_testtools_skip_decorator(logical_line):
"""Check that methods do not have the testtools.skip decorator
@@ -170,7 +163,8 @@
return True
-def get_resources_on_service_clients(logical_line, physical_line, filename,
+@core.flake8ext
+def get_resources_on_service_clients(physical_line, logical_line, filename,
line_number, lines):
"""Check that service client names of GET should be consistent
@@ -197,7 +191,8 @@
yield (0, msg)
-def delete_resources_on_service_clients(logical_line, physical_line, filename,
+@core.flake8ext
+def delete_resources_on_service_clients(physical_line, logical_line, filename,
line_number, lines):
"""Check that service client names of DELETE should be consistent
@@ -223,6 +218,7 @@
yield (0, msg)
+@core.flake8ext
def dont_import_local_tempest_into_lib(logical_line, filename):
"""Check that tempest.lib should not import local tempest code
@@ -244,6 +240,7 @@
yield (0, msg)
+@core.flake8ext
def use_rand_uuid_instead_of_uuid4(logical_line, filename):
"""Check that tests use data_utils.rand_uuid() instead of uuid.uuid4()
@@ -260,6 +257,7 @@
yield (0, msg)
+@core.flake8ext
def dont_use_config_in_tempest_lib(logical_line, filename):
"""Check that tempest.lib doesn't use tempest config
@@ -277,7 +275,8 @@
yield(0, msg)
-def dont_put_admin_tests_on_nonadmin_path(logical_line, physical_line,
+@core.flake8ext
+def dont_put_admin_tests_on_nonadmin_path(logical_line,
filename):
"""Check admin tests should exist under admin path
@@ -287,9 +286,6 @@
if 'tempest/api/' not in filename:
return
- if pycodestyle.noqa(physical_line):
- return
-
if not re.match(r'class .*Test.*\(.*Admin.*\):', logical_line):
return
@@ -298,6 +294,7 @@
yield(0, msg)
+@core.flake8ext
def unsupported_exception_attribute_PY3(logical_line):
"""Check Unsupported 'message' exception attribute in PY3
@@ -309,6 +306,7 @@
yield(0, msg)
+@core.flake8ext
def negative_test_attribute_always_applied_to_negative_tests(physical_line,
filename):
"""Check ``@decorators.attr(type=['negative'])`` applied to negative tests.
@@ -330,22 +328,3 @@
" to all negative API tests"
)
_HAVE_NEGATIVE_DECORATOR = False
-
-
-def factory(register):
- register(import_no_clients_in_api_and_scenario_tests)
- register(scenario_tests_need_service_tags)
- register(no_setup_teardown_class_for_tests)
- register(no_vi_headers)
- register(service_tags_not_in_module_path)
- register(no_hyphen_at_end_of_rand_name)
- register(no_mutable_default_args)
- register(no_testtools_skip_decorator)
- register(get_resources_on_service_clients)
- register(delete_resources_on_service_clients)
- register(dont_import_local_tempest_into_lib)
- register(dont_use_config_in_tempest_lib)
- register(use_rand_uuid_instead_of_uuid4)
- register(dont_put_admin_tests_on_nonadmin_path)
- register(unsupported_exception_attribute_PY3)
- register(negative_test_attribute_always_applied_to_negative_tests)
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/auth.py b/tempest/lib/auth.py
index 8e6d3d5..3fee489 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -684,7 +684,7 @@
def __str__(self):
"""Represent only attributes included in self.ATTRIBUTES"""
- attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
+ attrs = [attr for attr in self.ATTRIBUTES if attr != 'password']
_repr = dict((k, getattr(self, k)) for k in attrs)
return str(_repr)
@@ -741,7 +741,7 @@
def __str__(self):
"""Represent only attributes included in self.ATTRIBUTES"""
- attrs = [attr for attr in self.ATTRIBUTES if attr is not 'password']
+ attrs = [attr for attr in self.ATTRIBUTES if attr != 'password']
_repr = dict((k, getattr(self, k)) for k in attrs)
return str(_repr)
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/lib/services/volume/v3/__init__.py b/tempest/lib/services/volume/v3/__init__.py
index a1b7de3..e2fa836 100644
--- a/tempest/lib/services/volume/v3/__init__.py
+++ b/tempest/lib/services/volume/v3/__init__.py
@@ -11,6 +11,7 @@
# 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.volume.v3.attachments_client import AttachmentsClient
from tempest.lib.services.volume.v3.availability_zone_client \
import AvailabilityZoneClient
from tempest.lib.services.volume.v3.backups_client import BackupsClient
@@ -43,12 +44,11 @@
from tempest.lib.services.volume.v3.volume_manage_client import \
VolumeManageClient
from tempest.lib.services.volume.v3.volumes_client import VolumesClient
-
-__all__ = ['AvailabilityZoneClient', 'BackupsClient', 'BaseClient',
- 'CapabilitiesClient', 'EncryptionTypesClient', 'ExtensionsClient',
- 'GroupSnapshotsClient', 'GroupTypesClient', 'GroupsClient',
- 'HostsClient', 'LimitsClient', 'MessagesClient', 'QosSpecsClient',
- 'QuotaClassesClient', 'QuotasClient', 'SchedulerStatsClient',
- 'ServicesClient', 'SnapshotManageClient', 'SnapshotsClient',
- 'TransfersClient', 'TypesClient', 'VersionsClient',
- 'VolumeManageClient', 'VolumesClient']
+__all__ = ['AttachmentsClient', 'AvailabilityZoneClient', 'BackupsClient',
+ 'BaseClient', 'CapabilitiesClient', 'EncryptionTypesClient',
+ 'ExtensionsClient', 'GroupSnapshotsClient', 'GroupTypesClient',
+ 'GroupsClient', 'HostsClient', 'LimitsClient', 'MessagesClient',
+ 'QosSpecsClient', 'QuotaClassesClient', 'QuotasClient',
+ 'SchedulerStatsClient', 'ServicesClient', 'SnapshotManageClient',
+ 'SnapshotsClient', 'TransfersClient', 'TypesClient',
+ 'VersionsClient', 'VolumeManageClient', 'VolumesClient']
diff --git a/tempest/lib/services/volume/v3/attachments_client.py b/tempest/lib/services/volume/v3/attachments_client.py
new file mode 100644
index 0000000..5e448f7
--- /dev/null
+++ b/tempest/lib/services/volume/v3/attachments_client.py
@@ -0,0 +1,28 @@
+# 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
+from tempest.lib.services.volume import base_client
+
+
+class AttachmentsClient(base_client.BaseClient):
+ """Client class to send CRUD attachment V3 API requests"""
+
+ def show_attachment(self, attachment_id):
+ """Show volume attachment."""
+ url = "attachments/%s" % (attachment_id)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index cb7acbf..efdfe8e 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -966,18 +966,21 @@
# A port can have more than one IP address in some cases.
# If the network is dual-stack (IPv4 + IPv6), this port is associated
# with 2 subnets
- p_status = ['ACTIVE']
- # NOTE(vsaienko) With Ironic, instances live on separate hardware
- # servers. Neutron does not bind ports for Ironic instances, as a
- # result the port remains in the DOWN state.
- # TODO(vsaienko) remove once bug: #1599836 is resolved.
- if getattr(CONF.service_available, 'ironic', False):
- p_status.append('DOWN')
+
+ def _is_active(port):
+ # NOTE(vsaienko) With Ironic, instances live on separate hardware
+ # servers. Neutron does not bind ports for Ironic instances, as a
+ # result the port remains in the DOWN state. This has been fixed
+ # with the introduction of the networking-baremetal plugin but
+ # it's not mandatory (and is not used on all stable branches).
+ return (port['status'] == 'ACTIVE' or
+ port.get('binding:vnic_type') == 'baremetal')
+
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)]
+ _is_active(p))]
inactive = [p for p in ports if p['status'] != 'ACTIVE']
if inactive:
LOG.warning("Instance has ports that are not ACTIVE: %s", inactive)
@@ -1008,13 +1011,18 @@
port_id, ip4 = self._get_server_port_id_and_ip4(thing)
else:
ip4 = None
- result = client.create_floatingip(
- floating_network_id=external_network_id,
- port_id=port_id,
- tenant_id=thing['tenant_id'],
- fixed_ip_address=ip4
- )
+
+ kwargs = {
+ 'floating_network_id': external_network_id,
+ 'port_id': port_id,
+ 'tenant_id': thing['tenant_id'],
+ 'fixed_ip_address': ip4,
+ }
+ if CONF.network.subnet_id:
+ kwargs['subnet_id'] = CONF.network.subnet_id
+ result = client.create_floatingip(**kwargs)
floating_ip = result['floatingip']
+
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
client.delete_floatingip,
floating_ip['id'])
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index f46c7e8..d8584ec 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -346,10 +346,19 @@
network_id=CONF.network.public_network_id)['subnets']
if s['ip_version'] == 4
]
- self.assertEqual(1, len(v4_subnets),
- "Found %d IPv4 subnets" % len(v4_subnets))
- external_ips = [v4_subnets[0]['gateway_ip']]
+ if len(v4_subnets) > 1:
+ self.assertTrue(
+ CONF.network.subnet_id,
+ "Found %d subnets. Specify subnet using configuration "
+ "option [network].subnet_id."
+ % len(v4_subnets))
+ subnet = self.os_admin.subnets_client.show_subnet(
+ CONF.network.subnet_id)['subnet']
+ external_ips = [subnet['gateway_ip']]
+ else:
+ external_ips = [v4_subnets[0]['gateway_ip']]
+
self._check_server_connectivity(self.floating_ip_tuple.floating_ip,
external_ips)
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)
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
index 2774c44..8aed7d7 100644
--- a/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_application_credentials_client.py
@@ -20,78 +20,116 @@
class TestApplicationCredentialsClient(base.BaseServiceTest):
FAKE_CREATE_APP_CRED = {
"application_credential": {
- "description": "fake application credential",
+ "name": "monitoring",
+ "secret": "rEaqvJka48mpv",
+ "description": "Application credential for monitoring.",
+ "expires_at": "2018-02-27T18:30:59Z",
"roles": [
+ {"name": "Reader"}
+ ],
+ "access_rules": [
{
- "id": "c60fdd45",
- "domain_id": None,
- "name": "Member"
+ "path": "/v2.0/metrics",
+ "method": "GET",
+ "service": "monitoring"
}
],
- "expires_at": "2019-02-27T18:30:59.999999Z",
- "secret": "_BVq0xU5L",
- "unrestricted": None,
- "project_id": "ddef321",
- "id": "5499a186",
- "name": "one"
+ "unrestricted": False
}
}
FAKE_LIST_APP_CREDS = {
+ "links": {
+ "self": "http://example.com/identity/v3/users/" +
+ "fd786d56402c4d1691372e7dee0d00b5/application_credentials",
+ "previous": None,
+ "next": None
+ },
"application_credentials": [
{
- "description": "fake application credential",
+ "description": "Application credential for backups.",
"roles": [
{
"domain_id": None,
- "name": "Member",
- "id": "c60fdd45",
+ "name": "Writer",
+ "id": "6aff702516544aeca22817fd3bc39683"
}
],
- "expires_at": "2018-02-27T18:30:59.999999Z",
- "unrestricted": None,
- "project_id": "ddef321",
- "id": "5499a186",
- "name": "one"
+ "access_rules": [
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/users/" +
+ "fd786d56402c4d1691372e7dee0d00b5/" +
+ "application_credentials/" +
+ "308a7e905eee4071aac5971744c061f6"
+ },
+ "expires_at": "2018-02-27T18:30:59.000000",
+ "unrestricted": False,
+ "project_id": "231c62fb0fbd485b995e8b060c3f0d98",
+ "id": "308a7e905eee4071aac5971744c061f6",
+ "name": "backups"
},
{
- "description": None,
+ "description": "Application credential for monitoring.",
"roles": [
{
- "id": "0f1837c8",
+ "id": "6aff702516544aeca22817fd3bc39683",
"domain_id": None,
- "name": "anotherrole"
- },
- {
- "id": "c60fdd45",
- "domain_id": None,
- "name": "Member"
+ "name": "Reader"
}
],
- "expires_at": None,
- "unrestricted": None,
- "project_id": "c5403d938",
- "id": "d441c904f",
- "name": "two"
+ "access_rules": [
+ {
+ "path": "/v2.0/metrics",
+ "id": "07d719df00f349ef8de77d542edf010c",
+ "service": "monitoring",
+ "method": "GET"
+ }
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/users/" +
+ "fd786d56402c4d1691372e7dee0d00b5/" +
+ "application_credentials/" +
+ "58d61ff8e6e34accb35874016d1dba8b"
+ },
+ "expires_at": "2018-02-27T18:30:59.000000",
+ "unrestricted": False,
+ "project_id": "231c62fb0fbd485b995e8b060c3f0d98",
+ "id": "58d61ff8e6e34accb35874016d1dba8b",
+ "name": "monitoring"
}
]
}
FAKE_APP_CRED_INFO = {
"application_credential": {
- "description": None,
+ "description": "Application credential for monitoring.",
"roles": [
{
+ "id": "6aff702516544aeca22817fd3bc39683",
"domain_id": None,
- "name": "Member",
- "id": "c60fdd45",
+ "name": "Reader"
}
],
- "expires_at": None,
- "unrestricted": None,
- "project_id": "ddef321",
- "id": "5499a186",
- "name": "one"
+ "access_rules": [
+ {
+ "path": "/v2.0/metrics",
+ "id": "07d719df00f349ef8de77d542edf010c",
+ "service": "monitoring",
+ "method": "GET"
+ }
+ ],
+ "links": {
+ "self": "http://example.com/identity/v3/users/" +
+ "fd786d56402c4d1691372e7dee0d00b5/" +
+ "application_credentials/" +
+ "58d61ff8e6e34accb35874016d1dba8b"
+ },
+ "expires_at": "2018-02-27T18:30:59.000000",
+ "unrestricted": False,
+ "project_id": "231c62fb0fbd485b995e8b060c3f0d98",
+ "id": "58d61ff8e6e34accb35874016d1dba8b",
+ "name": "monitoring"
}
}
@@ -118,7 +156,7 @@
self.FAKE_APP_CRED_INFO,
bytes_body,
user_id="123456",
- application_credential_id="5499a186")
+ application_credential_id="58d61ff8e6e34accb35874016d1dba8b")
def _test_list_app_creds(self, bytes_body=False):
self.check_service_client_function(
@@ -152,5 +190,5 @@
'tempest.lib.common.rest_client.RestClient.delete',
{},
user_id="123456",
- application_credential_id="5499a186",
+ application_credential_id="58d61ff8e6e34accb35874016d1dba8b",
status=204)
diff --git a/tempest/tests/lib/services/image/v2/test_namespaces_client.py b/tempest/tests/lib/services/image/v2/test_namespaces_client.py
index 3b057ad..db1ffae 100644
--- a/tempest/tests/lib/services/image/v2/test_namespaces_client.py
+++ b/tempest/tests/lib/services/image/v2/test_namespaces_client.py
@@ -18,16 +18,64 @@
class TestNamespacesClient(base.BaseServiceTest):
- FAKE_CREATE_SHOW_NAMESPACE = {
- "namespace": "OS::Compute::Hypervisor",
- "visibility": "public",
- "description": "Tempest",
- "display_name": u"\u2740(*\xb4\u25e1`*)\u2740",
- "protected": True
+ FAKE_CREATE_NAMESPACE = {
+ "created_at": "2016-05-19T16:05:48Z",
+ "description": "A metadata definitions namespace.",
+ "display_name": "An Example Namespace",
+ "namespace": "FredCo::SomeCategory::Example",
+ "owner": "c60b1d57c5034e0d86902aedf8c49be0",
+ "protected": True,
+ "schema": "/v2/schemas/metadefs/namespace",
+ "self": "/v2/metadefs/namespaces/"
+ "FredCo::SomeCategory::Example",
+ "updated_at": "2016-05-19T16:05:48Z",
+ "visibility": "public"
+ }
+
+ FAKE_SHOW_NAMESPACE = {
+ "created_at": "2016-06-28T14:57:10Z",
+ "description": "The libvirt compute driver options.",
+ "display_name": "libvirt Driver Options",
+ "namespace": "OS::Compute::Libvirt",
+ "owner": "admin",
+ "properties": {
+ "boot_menu": {
+ "description": "If true, enables the BIOS bootmenu.",
+ "enum": [
+ "true",
+ "false"
+ ],
+ "title": "Boot Menu",
+ "type": "string"
+ },
+ "serial_port_count": {
+ "description": "Specifies the count of serial ports.",
+ "minimum": 0,
+ "title": "Serial Port Count",
+ "type": "integer"
+ }
+ },
+ "protected": True,
+ "resource_type_associations": [
+ {
+ "created_at": "2016-06-28T14:57:10Z",
+ "name": "OS::Glance::Image",
+ "prefix": "hw_"
+ },
+ {
+ "created_at": "2016-06-28T14:57:10Z",
+ "name": "OS::Nova::Flavor",
+ "prefix": "hw:"
+ }
+ ],
+ "schema": "/v2/schemas/metadefs/namespace",
+ "self": "/v2/metadefs/namespaces/OS::Compute::Libvirt",
+ "visibility": "public"
}
FAKE_LIST_NAMESPACES = {
- "first": "/v2/metadefs/namespaces?sort_key=created_at&sort_dir=asc",
+ "first": "/v2/metadefs/namespaces?sort_key=created_at&"
+ "sort_dir=asc",
"namespaces": [
{
"created_at": "2014-08-28T17:13:06Z",
@@ -89,7 +137,7 @@
self.check_service_client_function(
self.client.show_namespace,
'tempest.lib.common.rest_client.RestClient.get',
- self.FAKE_CREATE_SHOW_NAMESPACE,
+ self.FAKE_SHOW_NAMESPACE,
bytes_body,
namespace="OS::Compute::Hypervisor")
@@ -104,7 +152,7 @@
self.check_service_client_function(
self.client.create_namespace,
'tempest.lib.common.rest_client.RestClient.post',
- self.FAKE_CREATE_SHOW_NAMESPACE,
+ self.FAKE_CREATE_NAMESPACE,
bytes_body,
namespace="OS::Compute::Hypervisor",
visibility="public", description="Tempest",
diff --git a/tempest/tests/lib/services/image/v2/test_resource_types_client.py b/tempest/tests/lib/services/image/v2/test_resource_types_client.py
index 741b4eb..089e62e 100644
--- a/tempest/tests/lib/services/image/v2/test_resource_types_client.py
+++ b/tempest/tests/lib/services/image/v2/test_resource_types_client.py
@@ -48,6 +48,28 @@
]
}
+ FAKE_CREATE_RESOURCE_TYPE_ASSOCIATION = {
+ "created_at": "2020-03-07T18:20:44Z",
+ "name": "OS::Glance::Image",
+ "prefix": "hw:",
+ "updated_at": "2020-03-07T18:20:44Z"
+ }
+
+ FAKE_LIST_RESOURCE_TYPE_ASSOCIATION = {
+ "resource_type_associations": [
+ {
+ "created_at": "2020-03-07T18:20:44Z",
+ "name": "OS::Nova::Flavor",
+ "prefix": "hw:"
+ },
+ {
+ "created_at": "2020-03-07T18:20:44Z",
+ "name": "OS::Glance::Image",
+ "prefix": "hw_"
+ }
+ ]
+ }
+
def setUp(self):
super(TestResourceTypesClient, self).setUp()
fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -62,8 +84,48 @@
self.FAKE_LIST_RESOURCETYPES,
bytes_body)
+ def _test_create_resource_type_association(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.create_resource_type_association,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_CREATE_RESOURCE_TYPE_ASSOCIATION,
+ bytes_body, status=201,
+ namespace_id="OS::Compute::Hypervisor",
+ name="OS::Glance::Image", prefix="hw_",
+ )
+
+ def _test_list_resource_type_association(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.list_resource_type_association,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_LIST_RESOURCE_TYPE_ASSOCIATION,
+ bytes_body,
+ namespace_id="OS::Compute::Hypervisor",
+ )
+
def test_list_resource_types_with_str_body(self):
self._test_list_resource_types()
def test_list_resource_types_with_bytes_body(self):
self._test_list_resource_types(bytes_body=True)
+
+ def test_delete_resource_type_association(self):
+ self.check_service_client_function(
+ self.client.delete_resource_type_association,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ {}, status=204,
+ namespace_id="OS::Compute::Hypervisor",
+ resource_name="OS::Glance::Image",
+ )
+
+ def test_create_resource_type_association_with_str_body(self):
+ self._test_create_resource_type_association()
+
+ def test_create_resource_type_association_with_bytes_body(self):
+ self._test_create_resource_type_association(bytes_body=True)
+
+ def test_list_resource_type_association_with_str_body(self):
+ self._test_list_resource_type_association()
+
+ def test_list_resource_type_association_with_bytes_body(self):
+ self._test_list_resource_type_association(bytes_body=True)
diff --git a/tempest/tests/lib/services/object_storage/test_object_client.py b/tempest/tests/lib/services/object_storage/test_object_client.py
index a16d1d7..1749b03 100644
--- a/tempest/tests/lib/services/object_storage/test_object_client.py
+++ b/tempest/tests/lib/services/object_storage/test_object_client.py
@@ -69,7 +69,7 @@
# If the expected initial status is not 100, then an exception
# should be thrown and the connection closed
- if initial_status is 100:
+ if initial_status == 100:
status, reason = \
self.object_client.create_object_continue(cnt, obj, req_data)
else:
@@ -91,7 +91,7 @@
mock_poc.return_value.endheaders.assert_called_once_with()
# The following steps are only taken if the initial status is 100
- if initial_status is 100:
+ if initial_status == 100:
# Verify that the method returned what it was supposed to
self.assertEqual(status, 201)
diff --git a/tempest/tests/lib/services/volume/v3/test_attachments_client.py b/tempest/tests/lib/services/volume/v3/test_attachments_client.py
new file mode 100644
index 0000000..52c94e5
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_attachments_client.py
@@ -0,0 +1,46 @@
+# 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.volume.v3 import attachments_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+from oslo_utils.fixture import uuidsentinel as uuids
+
+
+class TestAttachmentsClient(base.BaseServiceTest):
+
+ FAKE_ATTACHMENT_INFO = {
+ "attachment": {
+ "status": "attaching",
+ "detached_at": "2015-09-16T09:28:52.000000",
+ "connection_info": {},
+ "attached_at": "2015-09-16T09:28:52.000000",
+ "attach_mode": "ro",
+ "instance": uuids.instance_id,
+ "volume_id": uuids.volume_id,
+ "id": uuids.id,
+ }
+ }
+
+ def setUp(self):
+ super(TestAttachmentsClient, self).setUp()
+ fake_auth = fake_auth_provider.FakeAuthProvider()
+ self.client = attachments_client.AttachmentsClient(fake_auth,
+ 'volume',
+ 'regionOne')
+
+ def test_show_attachment(self):
+ self.check_service_client_function(
+ self.client.show_attachment,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ATTACHMENT_INFO, attachment_id=uuids.id)
diff --git a/tempest/tests/test_hacking.py b/tempest/tests/test_hacking.py
index 83c1abb..7c31185 100644
--- a/tempest/tests/test_hacking.py
+++ b/tempest/tests/test_hacking.py
@@ -101,17 +101,6 @@
'def test_fake:', './tempest/scenario/orchestration/test_fake.py',
"\n"))
- def test_no_vi_headers(self):
- # NOTE(mtreinish) The lines parameter is used only for finding the
- # line location in the file. So these tests just pass a list of an
- # arbitrary length to use for verifying the check function.
- self.assertTrue(checks.no_vi_headers(
- '# vim: tabstop=4 shiftwidth=4 softtabstop=4', 1, range(250)))
- self.assertTrue(checks.no_vi_headers(
- '# vim: tabstop=4 shiftwidth=4 softtabstop=4', 249, range(250)))
- self.assertFalse(checks.no_vi_headers(
- '# vim: tabstop=4 shiftwidth=4 softtabstop=4', 149, range(250)))
-
def test_service_tags_not_in_module_path(self):
self.assertTrue(checks.service_tags_not_in_module_path(
"@utils.services('compute')",
diff --git a/test-requirements.txt b/test-requirements.txt
index 196387c..a50905f 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,7 +1,7 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
-hacking>=1.1.0,<1.2.0 # Apache-2.0
+hacking>=3.0,<3.1.0;python_version>='3.5' # Apache-2.0
mock>=2.0.0 # BSD
coverage!=4.4,>=4.0 # Apache-2.0
oslotest>=3.2.0 # Apache-2.0
diff --git a/tools/generate-tempest-plugins-list.py b/tools/generate-tempest-plugins-list.py
index 64adcbe..5ffef3e 100644
--- a/tools/generate-tempest-plugins-list.py
+++ b/tools/generate-tempest-plugins-list.py
@@ -36,6 +36,7 @@
# when the patches are merged.
BLACKLIST = [
'x/gce-api', # It looks gce-api doesn't support python3 yet.
+ 'x/glare', # To avoid sanity-job failure
'x/group-based-policy', # It looks this doesn't support python3 yet.
'x/intel-nfv-ci-tests', # https://review.opendev.org/#/c/634640/
'openstack/networking-generic-switch',
@@ -48,7 +49,9 @@
'openstack/neutron-dynamic-routing',
# https://review.opendev.org/#/c/637718/
'openstack/neutron-vpnaas', # https://review.opendev.org/#/c/637719/
+ 'x/tap-as-a-service', # To avoid sanity-job failure
'x/valet', # https://review.opendev.org/#/c/638339/
+ 'x/kingbird', # https://bugs.launchpad.net/kingbird/+bug/1869722
]
url = 'https://review.opendev.org/projects/'
diff --git a/tox.ini b/tox.ini
index 64921ef..e861c84 100644
--- a/tox.ini
+++ b/tox.ini
@@ -313,7 +313,6 @@
check-uuid --fix
[hacking]
-local-check-factory = tempest.hacking.checks.factory
import_exceptions = tempest.services
[flake8]
@@ -327,6 +326,26 @@
enable-extensions = H106,H203,H904
import-order-style = pep8
+[flake8:local-plugins]
+extension =
+ T102 = checks:import_no_clients_in_api_and_scenario_tests
+ T104 = checks:scenario_tests_need_service_tags
+ T105 = checks:no_setup_teardown_class_for_tests
+ T107 = checks:service_tags_not_in_module_path
+ T108 = checks:no_hyphen_at_end_of_rand_name
+ N322 = checks:no_mutable_default_args
+ T109 = checks:no_testtools_skip_decorator
+ T110 = checks:get_resources_on_service_clients
+ T111 = checks:delete_resources_on_service_clients
+ T112 = checks:dont_import_local_tempest_into_lib
+ T113 = checks:use_rand_uuid_instead_of_uuid4
+ T114 = checks:dont_use_config_in_tempest_lib
+ T115 = checks:dont_put_admin_tests_on_nonadmin_path
+ T116 = checks:unsupported_exception_attribute_PY3
+ T117 = checks:negative_test_attribute_always_applied_to_negative_tests
+paths =
+ ./tempest/hacking
+
[testenv:releasenotes]
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}